Imagine a simple form, which renders multiple custom components applying some layout tweaks.
The custom components are: <UsernameInput />
, <PasswordInput />
, <DateTimePicker />
, <FancyButton />
, <Checkbox />
And the JSX of the form component looks like:
return (
<View style={globalStyles.flex}>
<View style={styles.inputsContainer}>
<UsernameInput
... // a lot of props
/>
<PasswordInput
... // a lot of props
/>
<DateTimePicker
... // a lot of props
/>
</View>
<View style={styles.footer}>
<Checkbox />
<FancyButton type="submit" onPress={...} />
</View>
</View>
);
What if we try to split the component's render method into multiple inner sub-render functions?
const LoginForm = ({ ... }) => {
...
/**
* Renders the inputs of the form.
*
* @returns {React.ReactElement} The inputs.
*/
const renderInputs = () => (
<View style={styles.inputsContainer}>
<UsernameInput
... // a lot of props
/>
<PasswordInput
... // a lot of props
/>
<DateTimePicker
... // a lot of props
/>
</View>
);
/**
* Renders the submit button.
*
* @returns {React.ReactElement} The footer of the form.
*/
const renderFooter = () => (
<View style={styles.footer}>
<Checkbox />
<FancyButton type="submit" onPress={...} />
</View>
);
return (
{renderInputs()}
{renderFooter()}
);
};
Those helper methods returns JSX! They are not simple vanilla functions, they are components! WE ARE RECREATING COMPONENTS ON EACH RENDER!
In order to handle this, what I do is converting the inner functions to inner memoized components, like this:
/**
* Renders the submit button.
*
* @type {React.FC}
* @returns {React.ReactElement} The footer of the form.
*/
const Footer = useCallback(() => (
<View style={styles.footer}>
<Checkbox />
<FancyButton type="submit" onPress={...} />
</View>
), [deps]);
return (
<View style={globalStyles.flex}
<Inputs />
<Footer />
</View>
);
Is this considered an anti-pattern? I mean, should we avoid splitting the JSX when there is no reason to create a new component in the global scope of the app (in a new module or outside the component)?
CodePudding user response:
Is this considered an anti-pattern?
There's no point in using useCallback
on those functions if you don't provide those functions as props to other components that optimize their rendering by not re-rendering when their props don't change. (Lots of people misunderstand useCallback
, it's a bit subtle.) You're still recreating them every time (you have to create the function to pass it into useCallback
), you're just then doing even more work (processing deps
) to decide whether to use the newly-created function or the previously-created function. It's overhead without purpose, and not what useCallback
is for. The purpose of useCallback
is to create stable or stable-as-possible function references for passing to child components that may be able to avoid re-rendering if you don't change the functions they receive.
Instead, either:
Make those components (or just helper functions, depending) in your module. (No need to export them if they're only used by your component.) That way, they're only created once (when the module is loaded).
If you think about it, this is like any other function that's gotten too big: You break it into parts, and put those parts into smaller functions you call. Same concept.
Or
Create the functions once, on the first render (ensuring of course that they don't close over anything they need; pass it to them instead), storing the result on an object on a ref via
useRef
:const fnRef = useRef(null); if (!fn.Ref.current) { fnRef.current = { Footer: /*...*/, SomethingElse: /*...*/, // ... }; } const { Footer, SomethingElse /*, ...*/ } = fnRef.current; // ...
That way, they truly are created only once, on mount.
I would strongly lean toward #1.