Generating new component types in react hooks, how to maintain performance?

Viewed 669

I'm exploring a hook that generates a wrapper component to reduce the boilerplate of adding animations to my react-native app. I already wrote a hook that returns animated values and takes care of managing them internally, it works great. Each time I use it however, I have to write the boilerplate of applying the animated values like so:

//pseudocode
const [animatedValue, animationRef, ...otherUsefuLStuff] = useMyGenericAnimationHook(...some animation config)

return (
    <Animated.View style={{transform: [{translateY: animatedValue}]}}>
        .....
    </Animated.View>
)

This isn't a big deal, but it's still some boilerplate I have to put in for each animation. I was considering if I could return a wrapper component from my animation hook to abstract away this boilerplate, something like the following

//pseudocode
const [AnimatedTranslateY, animatedValue, animationRef, ...otherUsefulStuff] = useMyGenericAnimationHook(... some animation config)

return (
    <AnimatedTranslateY>.....</AnimatedTranslateY>
)

This looks cleaner, but returning a new component type from a hook definitely causes a problem. React will tear down and rebuild the children on each render because each time useMyAnimationHook() runs it will return a new component type!

My initial idea is to memoize the returned component type like so inside my hook

//pseudocode
const useMyGenericAnimationHook= (....configuration arguments) => {
    const animatedValueRef = ....create the animated value(s) ref(s)

    const WrapperView: React.FC<ViewProps> = useMemo(
        () =>
            React.memo(({ children, style, ...restProps }) => (
                <Animated.View {...restProps} 
                    style={[style, { transform: ...apply animated value(s)}]}>
                    {children}
                </Animated.View>
            )),
        [animatedValueRef.current]
    )

    return [WrapperView, animationHandle, otherUsefulStuff]
}

Here's where I'm somewhat confused. I think that this should work fine and not rebuild the tree on each render. The component type should remain stable unless the dependencies given to useMemo change at which point we want it to render anyway. I'm not totally confident about this however.

What will react's behavior be when using <WrapperView>?

Is there any reason why this isn't a good pattern?

Thanks for any insight!

1 Answers

Here's one more idea. I'm going to tweak your hook API a bit so that instead of returning new components (that just add props to the Animated.View component), you return a set of functions that return the wrapper props. Then, you call the functions you want and merge the results onto the style prop of a component. Example:

function useMyGenericAnimationHook(...animationConfig) {
  let animatedValue, animationRef, otherUsefulStuff;
  return {
    animatedValue,
    animationRef,
    ...otherUsefulStuff,
    // here's the meat of it
    translateY: () => ({
      transform: [{translateY: animatedValue}]
    }),
    color: () => ({
      color: `hsl(120,100%,${animatedValue}%)`
    }),
  };
}

function Component() {
  let { translateY, color } = useMyGenericAnimationHook(...animationConfig);
  return (
    <Animated.View style={{ ...translateY(), ...color() }}>
      {children}
    </Animated.View>
  );
}

Old Answer

I like this pattern--it looks very clean. As long as you make sure it doesn't generate a new component type each render it should be performant, and useMemo should work for that.

Another option would be to move the component outside the hook. If that works for your use case, it guarantees referential equality between renders. However, it doesn't let you bind props to the component, so the user must supply any required props.

//pseudocode
const WrapperView = React.memo(({ children, style, ...restProps }: ViewProps) => (
  <Animated.View {...restProps} 
                 style={[style, { transform: ...apply animated value(s)}]}>
                 {children}
  </Animated.View>
);

const useMyGenericAnimationHook= (....configuration arguments) => {
    const animatedValueRef = ....create the animated value(s) ref(s)

    return [WrapperView, animationHandle, otherUsefulStuff]
}

A third, but more unwieldy option would be to return JSX from the hook.

function useDiv() {
  return <div />;
}
function Component(props) {
  const div = useDiv();
  return (
    <main>{div}</main> // equivalent to <main><div/></main>
  )
}

You can essentially copy-paste JSX into the rendered output, which might suit your use case. I would only use this if neither of the first two options work for you, though.

Related