Prdufresne
Prdufresne

Reputation: 296

Using React.lazy to dynamically select a component

I'm working on an application that presents a list of tasks. When a user clicks on a task, it should present a form so that the user can update the status of that task. There are different types of tasks, however, and the correct form needs to be presented based on the task type. Each task type has a task definition with a field that identifies the form required for that task. When someone clicks on a task, the task object, including the task formComponent is passed to the formWrapper component. Each form component is in a JSX file and they are all exported as TaskForm.

I need to select the component based on the task definition. I thought I could accomplish this with react.Lazy by doing the following:

In the formWrapper's componentDidMount():

    // Store the path to the required JSX file in the state.
    this.setState( {formFile: '../Processes/'+this.state.task.formComponent} );

Then in formWrapper's render():

    TaskForm = React.lazy(() => import(this.state.formFile));
    return (
        <Suspense fallback={<div>Loading...</div>}>
             <TaskForm task={this.props.task} />
        </Suspense>

When I launch node.js, however it throws the warning "Critical dependency: the request of a dependency is an expression" and the form fails to load.

Am I even close to being on the right track? I can't statically define the forms. There are several of them and they are subject to change.

Upvotes: 0

Views: 1749

Answers (3)

Prdufresne
Prdufresne

Reputation: 296

My eventual solution, after the exchange with @backtick, was to place all the form JSX files in a folder with an index.js file that exported each of the forms in the folder (there is no other code in the file, just exports):

export { Form1 } from './Form1';
export { Form2 } from './Form2';

Then, in the document that needs to load one of the forms, I read the form name from the database and use it to lazy load the form in my render() function:

        const MyForm = React.lazy(() => import('../forms')
            .then(forms => (
                { default: forms[this.state.formFile] }
            ))
        );

The .then function exports the selected form as default so that only one component is stored in MyForm. If this.state.formFile = Form1, it loads Form1.

In the return() of my form, I include MyForm as a component:

                    <Suspense fallback={<div>Loading...</div>}>
                        <MyForm task={this.props.task} formVariables={this.state.formVariables}/>
                    </Suspense>

Upvotes: 0

Yash Joshi
Yash Joshi

Reputation: 2774

I suppose you want to dynamically import a component based on some condition. I have created a small function to do so:

const getComponent = (path) => {
  const Component = React.lazy(() => import(`${path}`));
  return Component;
};

// Usage

render() {
 const Component = getComponent(path);
 return <Component />;
}

Here is the working Sandbox: https://codesandbox.io/s/intelligent-snowflake-c0l6y?file=/src/App.js:306-344

Here is the live deployed app: https://csb-c0l6y.netlify.app/

You can confirm with network request that when we Load Component B a new fileis requested.

Note: I have not battle-tested this pattern. I think it may have some performance implication but should be easy to fix.

Upvotes: 0

backtick
backtick

Reputation: 2775

The code has to be statically analyzable because Webpack (which I believe is what you're using to bundle your app) needs to know what files are required so it can include them in the bundle. It isn't smart enough to take an expression like this.state.formFile.

I'm confident though that you're incorrect when you say you can't statically define the forms, or at least that it would be intolerable to do so. The files are enumerable, and all can be imported into this file.

Upvotes: 1

Related Questions