2

Please let me know if anything is not clear.

So I have this functional components that you pass in an array of Answer Components and it renders them in a quiz flow a bit like type form.

The component handles a lot of logic has this is used all over the app its in.

CCFlow a class component

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;

And now as function component, when built like this saveAnswer function and updateUsersAnswers contently re-render causing CCFlowAnswer to re-render

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;


Really lost here so any help would be appreciated, I am new to hooks so it could be something simple I am missing.

3 Answers 3

1

Since saveAnswer is being implemented within the functional component, anytime the Functional component re-renders, a new instance of saveAnswer function is created and being passed on to the CCFlowAnswer component causing it to also re-render even though no prop has actually changed.

To solve this, you should memoize the saveAnswer method using useCallback and use the functional pattern to update state

  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>
  );
Sign up to request clarification or add additional context in comments.

1 Comment

Hi Shubham, thank you for your answer. I tried wrapping this function in a useCallback but I hit a few snags. One the button no longer becomes un-disabled because the component doesn't re-render when the answer changes.
0

You should pass the function reference, instead of calling it in the 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;

Comments

0

So it turns out just wrapping things in useCallback doesn't work because I have other issues like button switching between disabled and active based on an answer being there.

I decided to re-write my component to have two states one that stores overall answers and the current answer in a separate state. This way I can wrap the save answer in a useCallback with only one dependency allowing for minmal re-renders but also my buttons become active/disabled.

Here is my full component if anyone is interested, hooks I find just take a little more thought when it comes to composition and splitting the state into small parts.

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;


Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.