【问题标题】:React Class vs Functional Component: When using hooks and functional components one of my functions constantly re-rendersReact 类与功能组件:当使用钩子和功能组件时,我的一个功能会不断重新渲染
【发布时间】:2020-01-28 07:46:29
【问题描述】:

如果有什么不清楚的地方请告诉我。

所以我有这个功能组件,你可以在一个答案组件数组中传递它,它会将它们呈现在一个有点像类型表单的测验流程中。

该组件处理很多逻辑,这在整个应用程序中都使用。

CCFlow 一个类组件

import React from "react";
import PropTypes from "prop-types";
import { Row, Col } from "react-bootstrap";

// CC
import CCProgressBar from "../CCProgressBar";
import CCButton from "../CCButton";
import CCFlowAnswer from "../CCFlowAnswer/";

// Local Assets and CSS
import "./CCFlow.css";

class CCFlow extends React.Component {
  constructor(props) {
    super(props);

    const usersAnswers = [];
    props.questions.forEach(() => usersAnswers.push(undefined));

    this.state = {
      currentQuestion: 0,
      usersAnswers: usersAnswers
    };
  }

  // Helpers
  initUsersAnswers = () => {
    const usersAnswers = [];
    this.props.questions.forEach(() => usersAnswers.push(undefined));
    return usersAnswers;
  };

  onLastQuestion = () => {
    return this.state.currentQuestion === this.props.questions.length - 1;
  };

  progress = () => {
    const total = 100 / this.props.questions.length;
    return Math.round(total * (this.state.currentQuestion + 1));
  };

  moveForward = () => {
    this.state.currentQuestion === this.props.questions.length - 1
      ? this.props.handleSubmit(this.state.usersAnswers)
      : this.setState({ currentQuestion: this.state.currentQuestion + 1 });
  };

  cleanAnswers = updatedAnswers => {
    this.props.wipeAnswers[this.state.currentQuestion]
      ? this.setState({
          usersAnswers: updatedAnswers.map((answer, index) =>
            index > this.state.currentQuestion ? undefined : answer
          )
        })
      : this.setState({ usersAnswers: updatedAnswers });
  };

  updateUsersAnswers = (key, answer) => {
    const updatedAnswers = [...this.state.usersAnswers];
    updatedAnswers[key] = answer;
    !this.props.wipeAnswers ||
    !this.props.wipeAnswers[this.state.currentQuestion]
      ? this.setState({ usersAnswers: updatedAnswers })
      : this.cleanAnswers(updatedAnswers);
  };

  handleNextButtonClick = () => {
    this.moveForward();
  };

  manualNextTrigger = () => {
    this.moveForward();
  };

  handleSkip = () => {
    this.updateUsersAnswers(this.state.currentQuestion, "None");
    this.moveForward();
  };

  handleBackButtonClick = () => {
    this.state.currentQuestion !== 0
      ? this.setState({ currentQuestion: this.state.currentQuestion - 1 })
      : window.history.back();
  };

  saveAnswer = (answer, answerKey) => {
    this.updateUsersAnswers(answerKey, answer);
  };

  render() {
    const { questions, style, answers } = this.props;
    const { currentQuestion, usersAnswers } = this.state;

    return (
      <div className="ccQuestions" style={style ? style : {}}>
        <Row>
          <Col xs={3}>
            <h4 style={{ minHeight: "80px" }}>{questions[currentQuestion]} </h4>
            <div id="ccFlowRow">
              <CCProgressBar
                width="200px"
                now={this.progress()}
              ></CCProgressBar>
              <span>{`${this.progress()}%`}</span>
            </div>
            <div id="ccFlowButtons">
              <CCButton variant="dark" onClick={this.handleBackButtonClick}>
                {currentQuestion === 0 ? "Exit" : "Back"}
              </CCButton>
              <CCButton
                style={{ marginLeft: "15px" }}
                variant={this.onLastQuestion() ? "primary" : "info"}
                onClick={this.handleNextButtonClick}
                disabled={usersAnswers[currentQuestion] ? false : true}
              >
                {this.onLastQuestion() ? "Create" : "Next"}
              </CCButton>
            </div>
          </Col>
          <Col xs={9}>
            <CCFlowAnswer
              FlowAnswer={answers[currentQuestion]}
              prevAnswer={
                currentQuestion !== 0 ? usersAnswers[currentQuestion - 1] : null
              }
              allAnswers={usersAnswers}
              handleAnswer={this.saveAnswer}
              questionIndex={currentQuestion}
              answer={
                usersAnswers[currentQuestion]
                  ? usersAnswers[currentQuestion]
                  : null
              }
              handleSkip={this.handleSkip}
              next={this.manualNextTrigger}
            />
          </Col>
        </Row>
      </div>
    );
  }
}

CCFlow.defaultProps = {
  questions: [],
  answers: [],
  wipeAnswers: []
};

CCFlow.propTypes = {
  style: PropTypes.object,
  questions: PropTypes.arrayOf(PropTypes.string),
  answers: PropTypes.arrayOf(PropTypes.elementType),
  handleSubmit: PropTypes.func,
  wipeAnswers: PropTypes.arrayOf(PropTypes.bool)
};

export default CCFlow;

现在作为函数组件,当像这样构建 saveAnswer 函数和 updateUsersAnswers 内容时重新渲染导致 CCFlowAnswer 重新渲染

import React, { useState } from "react";
import PropTypes from "prop-types";
import { Row, Col } from "react-bootstrap";

// CC
import CCProgressBar from "../CCProgressBar";
import CCButton from "../CCButton";
import CCFlowAnswer from "../CCFlowAnswer/";

// Local Assets and CSS
import "./CCFlow.css";

const CCFlow = ({ style, questions, answers, loaderLogic, handleSubmit }) => {
  // State
  const [currentQuestion, setCurrentQuestion] = useState(0);
  const [usersAnswers, setUsersAnswers] = useState(initUsersAnswers());

  // Helpers
  function initUsersAnswers() {
    const usersAnswers = {};
    questions.forEach((question, index) => {
      usersAnswers[`answer${index + 1}`] = null;
    });
    return usersAnswers;
  }

  function onLastQuestion() {
    return currentQuestion === questions.length - 1;
  }

  function progress() {
    const total = 100 / questions.length;
    return Math.round(total * (currentQuestion + 1));
  }

  function currentAnswerKey() {
    return `answer${currentQuestion + 1}`;
  }

  // Actions
  function handleNextButtonClick() {
    currentQuestion === questions.length - 1
      ? handleSubmit(usersAnswers)
      : setCurrentQuestion(currentQuestion + 1);
  }

  function handleBackButtonClick() {
    currentQuestion !== 0
      ? setCurrentQuestion(currentQuestion - 1)
      : window.history.back();
  }

  function saveAnswer(answer, answerKey) {
    setUsersAnswers({ ...usersAnswers, [answerKey]: answer });
  }

  return (
    <div className="ccQuestions" style={style ? style : {}}>
      <Row>
        <Col xs={3}>
          <h4 style={{ minHeight: "80px" }}>{questions[currentQuestion]} </h4>
          <div id="ccFlowRow">
            <CCProgressBar width="200px" now={progress()}></CCProgressBar>
            <span>{`${progress()}%`}</span>
          </div>
          <div id="ccFlowButtons">
            <CCButton variant="dark" onClick={handleBackButtonClick}>
              {currentQuestion === 0 ? "Exit" : "Back"}
            </CCButton>
            <CCButton
              style={{ marginLeft: "15px" }}
              variant={onLastQuestion() ? "primary" : "info"}
              onClick={handleNextButtonClick}
              disabled={usersAnswers[currentAnswerKey()] ? false : true}
            >
              {onLastQuestion() ? "Create" : "Next"}
            </CCButton>
          </div>
        </Col>
        <Col xs={9}>
          <CCFlowAnswer
            FlowAnswer={answers[currentQuestion]}
            loadBefore={loaderLogic[currentQuestion]}
            handleAnswer={answer =>
              saveAnswer(answer, `answer${currentQuestion + 1}`)
            }
            answer={
              usersAnswers[currentAnswerKey()]
                ? usersAnswers[currentAnswerKey()]
                : null
            }
          />
        </Col>
      </Row>
    </div>
  );
};

CCFlow.defaultProps = {
  questions: [],
  answers: [],
  waitForAnswers: []
};

CCFlow.propTypes = {
  style: PropTypes.object,
  questions: PropTypes.arrayOf(PropTypes.string),
  answers: PropTypes.arrayOf(PropTypes.elementType),
  loaderLogic: PropTypes.arrayOf(PropTypes.any),
  handleSubmit: PropTypes.func,
  waitForAnswers: PropTypes.arrayOf(PropTypes.bool)
};

export default CCFlow;


在这里真的迷路了,所以任何帮助都将不胜感激,我是钩子的新手,所以它可能是我缺少的一些简单的东西。

【问题讨论】:

    标签: javascript reactjs


    【解决方案1】:

    因为 saveAnswer 是在功能组件中实现的,所以无论何时功能组件重新渲染,都会创建一个新的 saveAnswer 函数实例并将其传递给CCFlowAnswer 组件,即使没有道具,它也会重新渲染实际上已经改变了。

    为了解决这个问题,你应该记住使用 useCallback 的 saveAnswer 方法,并使用函数模式来更新状态

      const saveAnswer = React.useCallback((answer) {
        const answerKey = `answer${currentQuestion + 1}`;
        setUsersAnswers(prevAns => ({ ...prevAns, [answerKey]: answer }));
      },[currentQuestion]); // changing only when currentQuestion changes
    
      return (
        <div className="ccQuestions" style={style ? style : {}}>
          <Row>
            <Col xs={3}>
              <h4 style={{ minHeight: "80px" }}>{questions[currentQuestion]} </h4>
              <div id="ccFlowRow">
                <CCProgressBar width="200px" now={progress()}></CCProgressBar>
                <span>{`${progress()}%`}</span>
              </div>
              <div id="ccFlowButtons">
                <CCButton variant="dark" onClick={handleBackButtonClick}>
                  {currentQuestion === 0 ? "Exit" : "Back"}
                </CCButton>
                <CCButton
                  style={{ marginLeft: "15px" }}
                  variant={onLastQuestion() ? "primary" : "info"}
                  onClick={handleNextButtonClick}
                  disabled={usersAnswers[currentAnswerKey()] ? false : true}
                >
                  {onLastQuestion() ? "Create" : "Next"}
                </CCButton>
              </div>
            </Col>
            <Col xs={9}>
              <CCFlowAnswer
                FlowAnswer={answers[currentQuestion]}
                loadBefore={loaderLogic[currentQuestion]}
                handleAnswer={saveAnswer}
                answer={
                  usersAnswers[currentAnswerKey()]
                    ? usersAnswers[currentAnswerKey()]
                    : null
                }
              />
            </Col>
          </Row>
        </div>
      );
    

    【讨论】:

    • 您好 Shubham,谢谢您的回答。我尝试将此函数包装在 useCallback 中,但遇到了一些障碍。一个按钮不再被禁用,因为当答案改变时组件不会重新渲染。
    【解决方案2】:

    您应该传递函数引用,而不是在 useState 中调用它。

    const CCFlow = ({ style, questions, answers, loaderLogic, handleSubmit }) => {
      // State
      const [currentQuestion, setCurrentQuestion] = useState(0);
      const [usersAnswers, setUsersAnswers] = useState( () => initUsersAnswers());
    
      // Helpers
      function initUsersAnswers() {
        const usersAnswers = {};
        questions.forEach((question, index) => {
          usersAnswers[`answer${index + 1}`] = null;
        });
        return usersAnswers;
      }
    
      function onLastQuestion() {
        return currentQuestion === questions.length - 1;
      }
    
      function progress() {
        const total = 100 / questions.length;
        return Math.round(total * (currentQuestion + 1));
      }
    
      function currentAnswerKey() {
        return `answer${currentQuestion + 1}`;
      }
    
      // Actions
      function handleNextButtonClick() {
        currentQuestion === questions.length - 1
          ? handleSubmit(usersAnswers)
          : setCurrentQuestion(currentQuestion + 1);
      }
    
      function handleBackButtonClick() {
        currentQuestion !== 0
          ? setCurrentQuestion(currentQuestion - 1)
          : window.history.back();
      }
    
      function saveAnswer(answer, answerKey) {
        setUsersAnswers({ ...usersAnswers, [answerKey]: answer });
      }
    
      return (
        <div className="ccQuestions" style={style ? style : {}}>
          <Row>
            <Col xs={3}>
              <h4 style={{ minHeight: "80px" }}>{questions[currentQuestion]} </h4>
              <div id="ccFlowRow">
                <CCProgressBar width="200px" now={progress()}></CCProgressBar>
                <span>{`${progress()}%`}</span>
              </div>
              <div id="ccFlowButtons">
                <CCButton variant="dark" onClick={handleBackButtonClick}>
                  {currentQuestion === 0 ? "Exit" : "Back"}
                </CCButton>
                <CCButton
                  style={{ marginLeft: "15px" }}
                  variant={onLastQuestion() ? "primary" : "info"}
                  onClick={handleNextButtonClick}
                  disabled={usersAnswers[currentAnswerKey()] ? false : true}
                >
                  {onLastQuestion() ? "Create" : "Next"}
                </CCButton>
              </div>
            </Col>
            <Col xs={9}>
              <CCFlowAnswer
                FlowAnswer={answers[currentQuestion]}
                loadBefore={loaderLogic[currentQuestion]}
                handleAnswer={answer =>
                  saveAnswer(answer, `answer${currentQuestion + 1}`)
                }
                answer={
                  usersAnswers[currentAnswerKey()]
                    ? usersAnswers[currentAnswerKey()]
                    : null
                }
              />
            </Col>
          </Row>
        </div>
      );
    };
    
    CCFlow.defaultProps = {
      questions: [],
      answers: [],
      waitForAnswers: []
    };
    
    CCFlow.propTypes = {
      style: PropTypes.object,
      questions: PropTypes.arrayOf(PropTypes.string),
      answers: PropTypes.arrayOf(PropTypes.elementType),
      loaderLogic: PropTypes.arrayOf(PropTypes.any),
      handleSubmit: PropTypes.func,
      waitForAnswers: PropTypes.arrayOf(PropTypes.bool)
    };
    
    export default CCFlow;
    

    【讨论】:

      【解决方案3】:

      所以事实证明,仅将内容包装在 useCallback 中是行不通的,因为我还有其他问题,例如根据那里的答案在禁用和活动之间切换按钮。

      我决定重新编写我的组件,使其具有两种状态,一种存储整体答案,另一种状态存储当前答案。这样,我可以将保存答案包装在 useCallback 中,只有一个依赖项允许最小的重新渲染,而且我的按钮也会变为活动/禁用。

      如果有人感兴趣,这里是我的完整组件,我发现挂钩在组合和将状态拆分为小部分时需要更多考虑。

      import React, { useState, useEffect, useCallback } from "react";
      import PropTypes from "prop-types";
      import { Row, Col } from "react-bootstrap";
      
      // CC
      import CCProgressBar from "../CCProgressBar";
      import CCButton from "../CCButton";
      import CCFlowAnswer from "../CCFlowAnswer/";
      
      // Local Assets and CSS
      import "./CCFlow.css";
      
      const CCFlow = ({ questions, answers, wipeAnswers, handleSubmit, style }) => {
        const [currentQuestion, setCurrentQuestion] = useState(0);
        const [usersAnswers, setUsersAnswers] = useState();
        const [currentAnswer, setCurrentAnswer] = useState(undefined);
        const [wipe, setWipe] = useState(false);
      
        useEffect(() => {
          setUsersAnswers(questions.map(() => undefined));
        }, [questions]);
      
        // Helpers
        function onLastQuestion() {
          return currentQuestion === questions.length - 1;
        }
      
        function progress() {
          const total = 100 / questions.length;
          return Math.round(total * (currentQuestion + 1));
        }
      
        function loadLastAnswer() {
          setCurrentAnswer(() => usersAnswers[currentQuestion - 1]);
          setCurrentQuestion(currentQuestion - 1);
        }
      
        function submitAnswers(answer, allAnswers, questionIndex) {
          const submittableAnswers = allAnswers;
          submittableAnswers[questionIndex] = answer;
          handleSubmit(submittableAnswers);
        }
      
        function cleanAnswers(allAnswers, wipeAnswers, wipe, questionIndex) {
          return wipe && wipeAnswers[questionIndex]
            ? allAnswers.map((answer, index) =>
                index > questionIndex ? undefined : answer
              )
            : allAnswers;
        }
      
        function loadNextAnswer(
          answer,
          allAnswers,
          wipeOptions,
          clear,
          questionIndex
        ) {
          const updatedUsersAnswers = cleanAnswers(
            allAnswers,
            wipeOptions,
            clear,
            questionIndex
          );
          updatedUsersAnswers[questionIndex] = answer;
          setWipe(false);
          setUsersAnswers(updatedUsersAnswers);
          setCurrentAnswer(
            updatedUsersAnswers[questionIndex + 1]
              ? updatedUsersAnswers[questionIndex + 1]
              : undefined
          );
          setCurrentQuestion(questionIndex + 1);
        }
      
        // Actions
        function moveForward(skip) {
          const ca = skip ? "None" : currentAnswer;
          currentQuestion === questions.length + 1
            ? submitAnswers(ca, usersAnswers, currentQuestion)
            : loadNextAnswer(ca, usersAnswers, wipeAnswers, wipe, currentQuestion);
        }
      
        function handleNextButtonClick() {
          moveForward();
        }
      
        function manualNextTrigger() {
          moveForward();
        }
      
        function handleSkip() {
          moveForward(true);
        }
      
        function handleBackButtonClick() {
          currentQuestion !== 0 ? loadLastAnswer() : window.history.back();
        }
      
        const saveAnswer = useCallback(answer => {
          setCurrentAnswer(answer);
          setWipe(() => true);
        }, []);
      
        return (
          <div className="ccQuestions" style={style ? style : {}}>
            <Row>
              <Col xs={3}>
                <h4 style={{ minHeight: "80px" }}>{questions[currentQuestion]} </h4>
                <div id="ccFlowRow">
                  <CCProgressBar width="200px" now={progress()}></CCProgressBar>
                  <span>{`${progress()}%`}</span>
                </div>
                <div id="ccFlowButtons">
                  <CCButton variant="dark" onClick={handleBackButtonClick}>
                    {currentQuestion === 0 ? "Exit" : "Back"}
                  </CCButton>
                  <CCButton
                    style={{ marginLeft: "15px" }}
                    variant={onLastQuestion() ? "primary" : "info"}
                    onClick={handleNextButtonClick}
                    disabled={currentAnswer ? false : true}
                  >
                    {onLastQuestion() ? "Create" : "Next"}
                  </CCButton>
                </div>
              </Col>
              <Col xs={9}>
                <CCFlowAnswer
                  FlowAnswer={answers[currentQuestion]}
                  prevAnswer={
                    currentQuestion !== 0 ? usersAnswers[currentQuestion - 1] : null
                  }
                  allAnswers={usersAnswers}
                  handleAnswer={saveAnswer}
                  answer={currentAnswer}
                  handleSkip={handleSkip}
                  next={manualNextTrigger}
                />
              </Col>
            </Row>
          </div>
        );
      };
      
      CCFlow.defaultProps = {
        questions: [],
        answers: [],
        wipeAnswers: []
      };
      
      CCFlow.propTypes = {
        style: PropTypes.object,
        questions: PropTypes.arrayOf(PropTypes.string),
        answers: PropTypes.arrayOf(PropTypes.elementType),
        handleSubmit: PropTypes.func,
        wipeAnswers: PropTypes.arrayOf(PropTypes.bool)
      };
      
      export default CCFlow;
      
      
      

      【讨论】:

        猜你喜欢
        • 2022-11-29
        • 1970-01-01
        • 2021-01-14
        • 2021-06-02
        • 2020-01-06
        • 2021-01-19
        • 1970-01-01
        • 2019-07-27
        • 2019-12-08
        相关资源
        最近更新 更多