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?
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
.