Today I Learned

React code-smells: Big form components

Big components suggest that there might be too much going on inside. However sometimes it is hard to remove/extract the logic, as it really belongs there. That’s the case often with form components. In such cases we can extract related groups of logic into hooks.

function QuestionForm({ defaultQuestion }) {
  const [questionData, setQuestionData] = useState(defaultQuestion);

  // ====== stuff related to image refresh ======

  const [imageTimer, setImageTimer] = useState({});

  useEffect(
    function refreshFrontendSignedImages() {
      // logic for refreshing
    },
    []
  );

  useEffect(function setupRefreshForInitialImages() {
    // logic to refresh setup
  }, []);

  // ====== stuff related to submitting ======

  const dispatch = useDispatch();

  const [questionValidationErrors, setQuestionValidationErrors] = useState([]);

  const cleanupSaveActions = useCallback(() => {
    // cleanup handler logic
  }, []);

  const onSubmit = useCallback(() => {
    // submit handler logic
  }, []);

  // ====== stuff related to handling changes ======

  const handleTitleChange = useCallback(({ target }) => {
    //  handle title change
  }, []);

  const handleChoicesUpdate = useCallback((choice, index) => {
    // handle choices update
  }, []);

  const handleTopicsUpdate = useCallback((topicIds) => {
    // handle topic update
  }, []);

  return <form>Some Form UI</form>;
}

After extracting imageRefresh, submitHandlers and changeHandlers logic to hooks we have certain benefits:

  • more readable component without dozens of functions flying around
  • functions are grouped by their utility, so it’s easier to reason about them as we don’t have submit functions surrounded by change handlers
  • whenever you need to work on part of the functionality (like image refresh) you have all functions in one place
  • hooks can be reused (but this is not the main point of this refactoring)
function QuestionForm({ defaultQuestion }) {
  const [questionData, setQuestionData] = useState(defaultQuestion);

  const { setImageTimer } = useSignedImagesRefresh({
    setQuestionData,
    defaultQuestion,
  });

  const {
    onSubmit,
    onSubmitAndCreateAnother,
    validationErrors,
  } = useQuestionFormSubmitHandlers({ questionData });

  const {
    handleTitleChange,
    handleChoicesUpdate,
    handleTopicsUpdate,
  } = useQuestionFormChangeHandlers({ setQuestionData });

  return <form>Some Form UI</form>;
}