Asha
Asha

Reputation: 4311

React Lazy loaded component loosing it's state (gets unmounted)

I have the below component that loads my components when required (upon route change).

function DynamicLoader(props) {
  const LazyComponent = React.lazy(() => import(`${props.component}`));
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <LazyComponent />
    </Suspense>
  );
}

And my Routes (using React-Router) looks like below:

            <Switch>
            {routes.map((prop, key) => {
              return (
                <Route
                  exact
                  path={prop.path}
                  render={() => (
                    <DynamicLoader component={prop.component} />
                  )}
                  key={key}
                />
              );
            })}
          </Switch>

This is working fine as far as mounting the component for each route goes, however it looks like with every change in the parent component, React is unmounting and remounting the lazy loaded component (instead of re-rendering). And this cause all the internal states to reset which is of course undesired. Can anyone recommend any solution please? Here is a codesandbox showing this issue.

Upvotes: 8

Views: 5758

Answers (2)

Ori Drori
Ori Drori

Reputation: 191976

Whenever the parent is rendered, the DynamicLoader recreates the LazyComponent. React sees a new component (not the same object), unmounts the previous one, and mounts to the new one.

To solve this issue use React.useMemo() inside DynamicLoader to memoize the current LazyComponent, and only recreates it if props.component actually changes:

const DynamicLoader = ({ component, parentUpdate }) => {
  const LazyComponent = useMemo(() => React.lazy(() => import(component)), [
    component
  ]);

  return (
    <Suspense fallback={<div>Loading...</div>}>
      <LazyComponent parentUpdate={parentUpdate} />
    </Suspense>
  );
};

Sandbox - to showcase the memoized LazyComponent, I'm passing the outer update to the HomeA component.

Since useMemo() caching is not guaranteed (react might free memory from time to time), you can write a simple lazy caching using a Map:

const componentsMap = new Map();

const useCachedLazy = (component) => {
  useEffect(
    () => () => {
      componentsMap.delete(component);
    },
    [component]
  );

  if (!componentsMap.has(component)) {
    componentsMap.set(
      component,
      React.lazy(() => import(component))
    );
  }

  return componentsMap.get(component);
};

const DynamicLoader = ({ component, parentUpdate }) => {
  const LazyComponent = useCachedLazy(component);

  return (
    <Suspense fallback={<div>Loading...</div>}>
      <LazyComponent parentUpdate={parentUpdate} />
    </Suspense>
  );
};

Sandbox

Upvotes: 13

Atakan Goktepe
Atakan Goktepe

Reputation: 110

I also had a similar issue while rendering dynamic fields. And I've fixed it by using @loadable/component, it accepts cache parameter that avoids rerendering if the component is already imported.

Here's a basic example that I've been using;

import React from "react";
import loadable from "@loadable/component";

interface Props {
  className?: string;
  field: "SelectBox" | "Aligner" | "Slider" | "Segment";
  fieldProps?: {
    [key: string]: any;
  };
  label: string;
  onChange: (value: string | number) => void;
  value: string | number | null | undefined;
}

const FieldComponent = loadable((props: Props) => import(`./${props.field}`), {
  cacheKey: (props: Props) => props.field,
});

export const DynamicField = ({
  className,
  field,
  fieldProps,
  label,
  onChange = () => null,
  value,
}: Props) => {
  return (
    <div>
      <label>{label}</label>
      <FieldComponent
        field={field}
        {...fieldProps}
        onChange={onChange}
        value={value}
      />
    </div>
  );
};

If you use @loadable/babel-plugin, dynamic properties are supported out of the box. Else you have to add cacheKey function: it takes props and returns a cache key.

Upvotes: 1

Related Questions