Repository: freeCodeCamp/freeCodeCamp
File: client/src/templates/Challenges/components/completion-modal.tsx (TypeScript)

Code before:
  t: TFunction;
}

interface DownloadableChallengeFile {
  name: string;
  ext: string;
  contents: string;
}

function CompletionModal({
  challengeFiles,
  close,
  dashedName,
  isOpen,
  isSignedIn,
  isSubmitting,
  message,
  submitChallenge,
  t
}: CompletionModalProps): JSX.Element {
  const downloadURL = useMemo(() => {
    if (!isOpen || !challengeFiles?.length) return null;
    const allFileContents = combineFileData(challengeFiles);
    const blob = new Blob([allFileContents], { type: 'text/json' });
    return URL.createObjectURL(blob);
  }, [isOpen, challengeFiles]);

  useEffect(() => {
    return () => {
      if (downloadURL) {
        URL.revokeObjectURL(downloadURL);
      }
    };
  }, [downloadURL]);

  useEffect(() => {
    return () => {
      close();
    };
  }, [close]);

  useEffect(() => {
    if (isOpen) {
      callGA({ event: 'pageview', pagePath: '/completion-modal' });
    }
  }, [isOpen]);

  const handleKeypress = useCallback(
    (e: React.KeyboardEvent) => {
      if (e.key === 'Escape') {
        e.stopPropagation();

Review comment (suggestion):
```suggestion
  const [downloadURL, setDownloadURL] = React.useState<string>();

  // We can't useMemo here, because it does not guarantee that the URL object
  // will be revoked when the dependencies change.
  useEffect(() => {
    // downloadURL is not in the dependency array because it should only change
    // if the challengeFiles change. It is in the useEffect so that we cannot
    // leak URL objects.
    if (downloadURL) URL.revokeObjectURL(downloadURL);
    if (challengeFiles?.length) {
      const allFileContents = combineFileData(challengeFiles);
      const blob = new Blob([allFileContents], { type: 'text/json' });
      setDownloadURL(URL.createObjectURL(blob));
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [challengeFiles]);
```
Now we always have a `downloadURL` (assuming there are files to download), but it's guaranteed to be cleaned up. Either by the second `useEffect` or by the first (when challenges change).

Code after:
  t: TFunction;
}

interface DownloadableChallengeFile {
  name: string;
  ext: string;
  contents: string;
}

function CompletionModal({
  challengeFiles,
  close,
  dashedName,
  isOpen,
  isSignedIn,
  isSubmitting,
  message,
  submitChallenge,
  t
}: CompletionModalProps): JSX.Element {
  const [downloadURL, setDownloadURL] = useState<string>();
  // We can't useMemo here, because it does not guarantee that the URL object
  // will be revoked when the dependencies change.
  useEffect(() => {
    // downloadURL is not in the dependency array because it should only change
    // if the challengeFiles change. It is in the useEffect so that we cannot
    // leak URL objects.
    if (downloadURL) URL.revokeObjectURL(downloadURL);
    if (challengeFiles?.length) {
      const allFileContents = combineFileData(challengeFiles);
      const blob = new Blob([allFileContents], { type: 'text/json' });
      setDownloadURL(URL.createObjectURL(blob));
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [challengeFiles]);

  useEffect(() => {
    return () => {
      close();
    };
  }, [close]);

  useEffect(() => {
    if (isOpen) {
      callGA({ event: 'pageview', pagePath: '/completion-modal' });
    }
  }, [isOpen]);

  const handleKeypress = useCallback(
    (e: React.KeyboardEvent) => {
      if (e.key === 'Escape') {