React Native - reduce render times to optimize performance while using React hooks

Viewed 794

Background


After releasing React v16.8, now we have hooks to use in React Native.
I am doing some simple tests to see the render times and the performance between
Hooked functional components and class components. Here is my sample:

@Components/Button.js

import React, { memo } from 'react';
import { TouchableOpacity, Text } from 'react-native';

const Button = memo(({ title, onPress }) => {
  console.log("Button render"); // check render times
  return (
    <TouchableOpacity onPress={onPress} disabled={disabled}>
      <Text>{title}</Text>
    </TouchableOpacity>
  );
});

export default Button;

@Contexts/User.js

import React, { createContext, useState } from 'react';
import User from '@Models/User';

export const UserContext = createContext({});
export const UserContextProvider = ({ children }) => {
  let [ user, setUser ] = useState(null);

  const login = (loginUser) => {
    if (loginUser instanceof User) { setUser(loginUser); }
  };

  const logout = () => {
    setUser(null);
  };

  return (
    <UserContext.Provider value={{value: user, login: login, logout: logout}}>
      {children}
    </UserContext.Provider>
  );
};

export function withUserContext(Component) {
  return function UserContextComponent(props) {
    return (
      <UserContext.Consumer>
        {(contexts) => <Component {...props} {...contexts} />}
      </UserContext.Consumer>
    );
  }
}

Cases


We have two cases below for constructing screen components:

@Screens/Login.js

Case 1: Functional Component with Hooks

import React, { memo, useContext, useState } from 'react';
import { View, Text } from 'react-native';

import Button from '@Components/Button';
import { UserContext } from '@Contexts/User';

const LoginScreen = memo(({ navigation }) => {
  const appUser = useContext(UserContext);
  const [foo, setFoo] = useState(false);

  const userLogin = async () => {
    let response = await fetch('blahblahblah');
    if (response.is_success) {
      appUser.login(user);
    } else {
      // fail on login, error handling
    }
  };

  const toggleFoo = () => {
    setFoo(!foo);
    console.log("current foo", foo);
  };

  console.log("render Login Screen"); // check render times
  return (
    <View>
      <Text>Login Screen</Text>
      <Button onPress={userLogin} title="Login" />
      <Button onPress={toggleFoo} title="Toggle Foo" />
    </View>
  );
});

export default LoginScreen;

Case 2: Component wrapped with HOC

import React, { Component } from 'react';
import { View, Text } from 'react-native';

import Button from '@Components/Button';
import { withUserContext } from '@Contexts/User';
import UserService from '@Services/User';

class LoginScreen extends Component {
  state = { foo: false };

  userLogin = async () => {
    let response = await UserService.login();
    if (response.is_success) {
      login(user);      // function from UserContext
    } else {
      // fail on login, error handling
    }
  };

  toggleFoo = () => {
    const { foo } = this.state;
    this.setState({ foo: !foo });
    console.log("current foo", foo);
  };

  render() {
    console.log("render Login Screen"); // check render times
    return (
      <View>
        <Text>Login Screen</Text>
        <Button onPress={userLogin} title="Login" />
        <Button onPress={toggleDisable} title="Toggle" />
      </View>
    );
  }
}

Results


Both cases have identical render times at the beginning:

render Login Screen
Button render
Button render

But while I press the "Toggle" button, the state changed and here is the result:

Case 1: Functional Component with Hooks

render Login Screen
Button render
Button render

Case 2: Component wrapped with HOC

render Login Screen

Questions


Although the Button Component isn't a large bunch of codes, considering the re-render times between two cases, Case 2 should have a better performance than Case 1. However, considering the code readability, I definitely love using hooks more than using HOC. (Especially the function: appUser.login() and login())

So here's the question. Is there any solution that I can keep the benefits of both size, decreasing the re-render times while using the hooks? Thank you.

2 Answers

The reason is in functional component whenever the component re-render, new userLogin created => Button component is re-render.

const userLogin = async () => {
    const response = await fetch("blahblahblah")
    if (response.is_success) {
      appUser.login(user)
    } else {
      // fail on login, error handling
    }
 }

You can use useCallback to memoize userLogin function + wrap Button component with React.memo (as what you did) prevent unwanted re-render:

const userLogin = useCallback(async () => {
    const response = await fetch("blahblahblah")
    if (response.is_success) {
      appUser.login(user)
    } else {
      // fail on login, error handling
    }
}, [])

The reason why it not happen in class component is when class component is re-rendered only render function is trigger (of course some other lifecycle function such as shoudlComponentUpdate, componentDidUpdate trigger too). ==> userLogin not change ==> Button component is not re-render.

This is great article to have a look about useCallback + memo

Notice: When you use Context, memo can not prevent the component, which is a Consumer, re-render if values of Context Provider changed. For example: If you call setUser in UserContext => UserContext re-render => value={{value: user, login: login, logout: logout}} change => LoginScreen re-render. You cannot use shouldComponentUpdate (class compoenent) or memo (functional component) to prevent re-render, because it's not update via props, it's updated via value of Context Provide

The reason that both button re-render even though you use memo in case of a functional component is because the function references are changed on each re-render as they are defined within the functional component.

Similar case will happen if you use arrow functions in render for a class component

In case of a class the function references don't change with the way you define them as functions are defined outside of your render method

To optimize on rerenders, you should make use of useCallback hook to memoize your function references

const LoginScreen = memo(({ navigation }) => {
  const appUser = useContext(UserContext);
  const [foo, setFoo] = useState(false);

  const userLogin = useCallback(async () => {
    let response = await fetch('blahblahblah');
    if (response.is_success) {
      appUser.login(user);
    } else {
      // fail on login, error handling
    }
  }, []); // Add dependency if need i.e when using value from closure

  const toggleFoo = useCallback(() => {
    setFoo(prevFoo => !prevFoo); // use functional state here
  }, []);

  console.log("render Login Screen"); // check render times
  return (
    <View>
      <Text>Login Screen</Text>
      <Button onPress={userLogin} title="Login" />
      <Button onPress={toggleFoo} title="Toggle Foo" />
    </View>
  );
});

export default LoginScreen;

Also note that React.memo cannot prevent re-renders due to context value changes. Also note that while passing value to context provider you should make use of useMemo too

export const UserContextProvider = ({ children }) => {
  let [ user, setUser ] = useState(null);

  const login = useCallback((loginUser) => {
    if (loginUser instanceof User) { setUser(loginUser); }
  }, []);

  const logout = useCallback(() => {
    setUser(null);
  }, []);

  const value = useMemo(() => ({
     value: user,
     login: login,
     logout: logout,
  }), [user, login, logout]); 
  /*
     Note that login and logout functions are implemented using `useCallback` and 
     are created on initial render only and hence adding them as dependency here 
     doesn't make a difference and will definitely not lead to new referecne for 
      value. Only `user` value change will create a new object reference
  */
  return (
    <UserContext.Provider value={value}>
      {children}
    </UserContext.Provider>
  );
};
Related