Search code examples
javascriptreactjsmemoization

Is it safe to useMemo for JSX?


I'm trying to do a memoize Modal and I have a problem here.

When I change input I dont need to re-render the Modal component.

For example:

Modal.tsx looks like this:

import React from "react";
import { StyledModalContent, StyledModalWrapper, AbsoluteCenter } from "../../css";

interface ModalProps {
  open: boolean;
  onClose: () => void;
  children: React.ReactNode
};

const ModalView: React.FC<ModalProps> = ({ open, onClose, children }) => {
  console.log("modal rendered");
  
  return (
    <StyledModalWrapper style={{ textAlign: "center", display: open ? "block" : "none" }}>
      <AbsoluteCenter>
        <StyledModalContent>
          <button
            style={{
              position: "absolute",
              cursor: "pointer",
              top: -10,
              right: -10,
              width: 40,
              height: 40,
              border: 'none',
              boxShadow: '0 10px 10px 0 rgba(0, 0, 0, 0.07)',
              backgroundColor: '#ffffff',
              borderRadius: 20,
              color: '#ba3c4d',
              fontSize: 18
            }}
            onClick={onClose}
          >
            X
          </button>
          {open && children}
        </StyledModalContent>
      </AbsoluteCenter>
    </StyledModalWrapper>
  );
};

export default React.memo(ModalView);

Here is an example of how I wrap it.

import React from 'react'
import Modal from './modal';

const App: React.FC<any> = (props: any) => {
  const [test, setTest] = React.useState("");
  const [openCreateChannelDialog, setOpenCreateChannelDialog] = React.useState(false);
    
  const hideCreateModalDialog = React.useCallback(() => {
    setOpenCreateChannelDialog(false);
  }, []);

  return (
    <>
      <input type="text" value={test} onChange={(e) => setTest(e.target.value)} />
      <button onClick={() => setOpenCreateChannelDialog(true)}>Create channel</button>
    
      <Modal
        open={openCreateChannelDialog}
        onClose={hideCreateModalDialog}
        children={<CreateChannel onClose={hideCreateModalDialog} />}
      />
    </>
};

I know, Modal re-rendered because children reference created every time when App component re-renders (when I change an input text).

Know I'm interested, if I wrap <CreateChannel onClose={hideCreateModalDialog} /> inside React.useMemo() hook

For example:

  const MemoizedCreateChannel = React.useMemo(() => {
    return <CreateChannel onClose={hideCreateModalDialog} />
  }, [hideCreateModalDialog]);

And change children props inside Modal

from:

children={<CreateChannel onClose={hideCreateModalDialog} />}

to

children={MemoizedCreateChannel}

It works fine, but is it safe? And it is only one solution that tried to memoize a Modal?


Solution

  • Memoizing JSX expressions is part of the official useMemo API:

    const Parent = ({ a }) => useMemo(() => <Child1 a={a} />, [a]); 
    // This is perfectly fine; Child re-renders only, if `a` changes
    

    useMemo memoizes individual children and computed values, given any dependencies. You can think of memo as a shortcut of useMemo for the whole component, that compares all props.

    But memo has one flaw - it doesn't work with children:

    const Modal = React.memo(ModalView);
    
    // React.memo won't prevent any re-renders here
    <Modal>
      <CreateChannel />
    </Modal>
    

    children are part of the props. And React.createElement always creates a new immutable object reference (REPL). So each time memo compares props, it will determine that children reference has changed, if not a primitive.

    To prevent this, you can either use useMemo in parent App to memoize children (which you already did). Or define a custom comparison function for memo, so Modal component now becomes responsible for performance optimization itself. react-fast-compare is a handy library to avoid boiler plate for areEqual.