reactjsrenderingsetstate

setState not triggering a re-render when data has been modified(state type is variable)


I changed a state with setState, but it did not trigger a re-render. I considered it a minor issue, so I searched extensively, but I couldn't find a solution.

I want to check the second and third checkboxes when the first one is checked, and also uncheck them.

Here is my code:

import React, { useState, useEffect } from 'react';
import styled from 'styled-components';

import Button from '@components/common/Button';
import Input from '@components/common/Input';
import CheckBox from '@components/common/CheckBox';

import { LuChevronDown, LuChevronUp } from 'react-icons/lu';

// 임시 데이터
import { tempTerms } from '@constants/tempData';

const BasicRegistration: React.FC = () => {
  const [agreeChecked, setAgreeChecked] = useState(false);
  const [agreeChecked2, setAgreeChecked2] = useState(false);
  const [agreeChecked3, setAgreeChecked3] = useState(false);
  const [viewFullTerms, setViewFullTerms] = useState(false); // 전문 보기

  // 체크박스 핸들러 - 이름 변경 필요
  const handleCheckBoxChange = () => {
    setAgreeChecked((prevState) => !prevState);
  };

  useEffect(() => {
    if (agreeChecked) {
      setAgreeChecked2(true);
      setAgreeChecked3(true);
    } else {
      setAgreeChecked2(false);
      setAgreeChecked3(false);
    }
  }, [agreeChecked]);

  useEffect(
    () => {
      console.log(agreeChecked, agreeChecked2, agreeChecked3);
    },
    [agreeChecked2, agreeChecked3]
  );

  const handleCheckBoxChange2 = () => {
    setAgreeChecked2((prevState) => !prevState);
  };
  const handleCheckBoxChange3 = () => {
    setAgreeChecked3((prevState) => !prevState);
  };

  // 전문 보기
  const handleViewFullTerms = () => {
    setViewFullTerms((prevState) => !prevState);
  };

  return (
    <Container>
      <Section>
        <Heading>아이디</Heading>
        <Input />
      </Section>

      <Section>
        <Heading>회원정보 입력</Heading>
        <P>닉네임*</P>
        <Input />
        <P>핸드폰 번호*</P>
        <Input />
      </Section>

      <Section>
        <Heading>이용약관 동의</Heading>
        <Hr />
        <CheckBox
          onChange={handleCheckBoxChange}
          value="agree"
          checked={agreeChecked}
          text="모두 동의합니다."
          textColor="black"
        />
        <SmallP>만 14세 이상(필수), 이용약관(필수)</SmallP>

        <Hr />
        <CheckBox
          onChange={handleCheckBoxChange2}
          value="agree2"
          checked={agreeChecked2}
          text="(필수) 만 14세 이상입니다."
          textColor="black"
        />
        <CheckBox
          onChange={handleCheckBoxChange3}
          value="agree3"
          checked={agreeChecked3}
          text="(필수) 이용 약관 동의"
          textColor="black"
        />
        <TermCheckContainer>
          {viewFullTerms && <LuChevronUp />}
          {!viewFullTerms && <LuChevronDown />}
          <Button
            buttonStyle="link"
            buttonSize="sm"
            onClick={handleViewFullTerms}
          >
            전문 보기
          </Button>
        </TermCheckContainer>
        {viewFullTerms && (
          <TermContainer>
            <P2>{tempTerms}</P2>
          </TermContainer>
        )}
      </Section>

      <Section>
        <div>
          <Button buttonStyle="square-green" buttonSize="md">
            가입하기
          </Button>
        </div>
      </Section>
    </Container>
  );
};

export default BasicRegistration;

const Container = styled.div`
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: flex-start;
  width: 80%;

  & > * {
    width: 100%;
  }
`;

const Section = styled.div`
  display: flex;
  flex-direction: column;
  padding: 10px 0;

  & > * {
    margin: 4px 0;
  }

  input {
    width: 100%;
  }
`;

const Heading = styled.p`
  font-size: var(--font-size-hd-1);
  padding: 4px 0;
`;

const SmallP = styled.p`
  font-size: var(--font-size-sm-1);
  color: var(--color-grey-1);
`;

const P = styled.p``;

const Hr = styled.hr`
  border: 0;
  border-top: 1px solid var(--color-grey-2);
`;

const P2 = styled.p`
  white-space: pre-wrap;
`;

const TermCheckContainer = styled.div`
  padding-top: 8px;
  display: flex;
`;

const TermContainer = styled.div`
  padding: 12px;
  border: solid black 0.5px;
  font-size: var(--font-size-sm-1);
  line-height: 1.5;
  color: var(--color-grey-1);
`;

Here is child component(CheckBox):

import React, { useState, useEffect } from 'react';
import styled, { css } from 'styled-components';

// 체크박스 텍스트 컬러 props
interface StyledCheckBoxProps {
  textColor?: 'grey' | 'black';
}

const textColors = {
  grey: css`
    color: var(--color-grey-1);
  `,
  black: css`
    color: var(--color-black);
  `,
};

interface CheckBoxProps extends StyledCheckBoxProps {
  value: string;
  checked: boolean;
  text: string;
  onChange: (checked: boolean) => void;
}

const CheckBox: React.FC<CheckBoxProps> = ({
  value,
  checked = false,
  text,
  onChange,
  textColor = 'grey'
}) => {
  const [isChecked, setIsChecked] = useState(checked);

  const handleCheck = () => {
    const newChecked = !isChecked;
    setIsChecked(newChecked);
    onChange(newChecked);
  };

  useEffect(() => {
    console.log(isChecked)
  }, [isChecked])
  

  return (
    <Label htmlFor={value} className="chk_box">
      <Input
        type="checkbox"
        id={value}
        checked={isChecked}
        onChange={handleCheck}
      />
      <TextContainer>
        <Span className={isChecked ? 'on' : ''} />
        <Text textColor={textColor}>{text}</Text>
      </TextContainer>
    </Label>
  );
};

export default CheckBox;

const Label = styled.label`
  display: inline-block;
  position: relative;
  cursor: pointer;
`;

const Input = styled.input`
  display: none;
`;

const TextContainer = styled.div`
  display: flex;
  align-items: center;
`;

const Span = styled.span`
  width: 16px;
  height: 16px;
  border: 1px solid var(--color-grey-2);
  position: relative;
  margin-right: 4px;

  &.on {
    background-color: var(--color-green-main);
    display: flex;
    justify-content: center;
    align-items: center;

    &::after {
      content: '';
      width: 6px;
      height: 9px;
      border: solid var(--color-white);
      border-width: 0 2px 2px 0;
      transform: rotate(45deg);
      position: absolute;
      top: 0px;
    }
  }
`;

const Text = styled.p<StyledCheckBoxProps>`
  margin: 0;
  font-size: var(--font-size-ft-1);
  ${(props) => props.textColor && textColors[props.textColor]}
  font-weight: var(--font-weight-regular);
`;

I searched a lot but I couldn't find any solution. I'm a complete beginner, so there might be things I'm missing. Can you help me out?


Solution

  • This is because useState is going to initialize with a value and the only way to set that value is through setState

    In checkbox component you are doing this thing

    const [isChecked, setIsChecked] = useState(checked);

    be careful with doing this (initialize a state from a prop) because you can imagine that if the prop change the useState will be aware of that and will change is checked but is not going to work that way

    Why don't you better pass directly the prop to the component that is going to use that prop instead of create a new state

    Checkbox component refactored

    const CheckBox: React.FC<CheckBoxProps> = ({
      value,
      checked = false,
      text,
      onChange,
      textColor = 'grey'
    }) => {
      return (
        <Label htmlFor={value} className="chk_box">
          <Input
            type="checkbox"
            id={value}
            checked={checked}
            // the onChange prop is already doing the thing that you need
            onChange={onChange}
          />
          <TextContainer>
            <Span className={checked ? 'on' : ''} />
            <Text textColor={textColor}>{text}</Text>
          </TextContainer>
        </Label>
      );
    };
    

    Regarding the other file try not to use useEffect always -> only when it's necessary the way I think of it is: "how can I achieve this without an effect -> if not -> use an effect"

    Most of people put a lot of useEffects when they are not strictly necessary

    const BasicRegistration: React.FC = () => {
      const [agreeChecked, setAgreeChecked] = useState(false);
      const [agreeChecked2, setAgreeChecked2] = useState(false);
      const [agreeChecked3, setAgreeChecked3] = useState(false);
      const [viewFullTerms, setViewFullTerms] = useState(false); // 전문 보기
    
      // 체크박스 핸들러 - 이름 변경 필요
      const handleCheckBoxChange = () => {
        setAgreeChecked(!agreeChecked);
        setAgreeChecked2(!agreeChecked);
        setAgreeChecked3(!agreeChecked);
      };
    
      const handleCheckBoxChange2 = () => {
        setAgreeChecked2((prevState) => !prevState);
      };
      const handleCheckBoxChange3 = () => {
        setAgreeChecked3((prevState) => !prevState);
      };
    
      // 전문 보기
      const handleViewFullTerms = () => {
        setViewFullTerms((prevState) => !prevState);
      };
    
      return (
        <Container>
          <Section>
            <Heading>아이디</Heading>
            <Input />
          </Section>
    
          <Section>
            <Heading>회원정보 입력</Heading>
            <P>닉네임*</P>
            <Input />
            <P>핸드폰 번호*</P>
            <Input />
          </Section>
    
          <Section>
            <Heading>이용약관 동의</Heading>
            <Hr />
            <CheckBox
              onChange={handleCheckBoxChange}
              value="agree"
              checked={agreeChecked}
              text="모두 동의합니다."
              textColor="black"
            />
            <SmallP>만 14세 이상(필수), 이용약관(필수)</SmallP>
    
            <Hr />
            <CheckBox
              onChange={handleCheckBoxChange2}
              value="agree2"
              checked={agreeChecked2}
              text="(필수) 만 14세 이상입니다."
              textColor="black"
            />
            <CheckBox
              onChange={handleCheckBoxChange3}
              value="agree3"
              checked={agreeChecked3}
              text="(필수) 이용 약관 동의"
              textColor="black"
            />
            <TermCheckContainer>
              {viewFullTerms && <LuChevronUp />}
              {!viewFullTerms && <LuChevronDown />}
              <Button
                buttonStyle="link"
                buttonSize="sm"
                onClick={handleViewFullTerms}
              >
                전문 보기
              </Button>
            </TermCheckContainer>
            {viewFullTerms && (
              <TermContainer>
                <P2>{tempTerms}</P2>
              </TermContainer>
            )}
          </Section>
    
          <Section>
            <div>
              <Button buttonStyle="square-green" buttonSize="md">
                가입하기
              </Button>
            </div>
          </Section>
        </Container>
      );
    };