javascriptreactjsreact-routerreact-suspense

Prevent re-render of Await in React Suspense


I am using Suspense from React, and Await from React-Router-DOM to wait for a promise and return JSX on resolve, but in most cases I want to call some functions like navigate or something else, then the functions are called twice. I want to prevent that.

Loader

export const Loader = async ({ params }) => {
  CheckAuth();
  let fetch_data = fetchApi('auth/activate-account', 'POST', { verify_token: params.verify_token });
  return defer({ loaderPromise: fetch_data });
}
// this loader is called in the Route loader prop
const VerifyToken = () => {            
  const loaderData = useLoaderData();
            
  const navigate = useNavigate();
            
  const { showAlert } = useGeneral();
            
  return (
    <React.Suspense fallback={<PreLoader />}>
      <Await resolve={loaderData.loaderPromise}>
        {(resolvedData) => {
          if (resolvedData.type === "warning") {
            showAlert(resolvedData.type, resolvedData.msg);
            navigate('/Auth/Login', { replace: true });
            return null;
          }
          return (
            { JSX }
          );
        }}
      </Await>
    </React.Suspense>
  );
};

This causes calling the navigate and showAlert functions twice. I want to prevent that or use another approach or other methods.

I tried this:

let loaderRef = React.useRef(false);
    
<React.Suspense fallback={<PreLoader />}>
  <Await resolve={loaderData.loaderPromise}>
    {(resolvedData) => {
      if (!loaderRef.current) {
        showAlert(resolvedData.type, resolvedData.msg);
        loaderRef.current = true;
        setTimeout(() => {
          navigate('/Auth/Login', { replace: true });
        }, 0);
        return null;
      }
    }}
  </Await>
</React.Suspense>

This works but not efficient in most cases.


Solution

  • I suspect there is some rendering issue being exposed by a React.StrictMode component higher up the ReactTree, specific lifecycle functions are double-invoked in non-production builds to expose logical issues in your code like unintentional side-effects.

    Abstract the unintentional side-effect of showing the alert and navigating into a React component that can use the useEffect hook to correctly issue an intentional side-effect to show the alert and conditionally redirect or render the specified JSX content.

    Example:

    const DataHandler = ({ children, resolvedData }) => {
      const { showAlert } = useGeneral();
    
      useEffect(() => {
        if (resolvedData.type === "warning") {
          showAlert(resolvedData.type, resolvedData.msg);
        }
      }, [resolvedData]);
    
      if (resolvedData.type === "warning") {
        return <Navigate to='/Auth/Login' replace />;
      }
    
      return children;
    };
    
    const VerifyToken = () => {        
      const loaderData = useLoaderData();
      
      return (
        <React.Suspense fallback={<PreLoader />}>
          <Await resolve={loaderData.loaderPromise}>
            {(resolvedData) => {
              return (
                <DataHandler resolvedData={resolvedData}>
                  { JSX }
                </DataHandler>
              );
            }}
          </Await>
        </React.Suspense>
      );
    };