I was looking for implementations for a Factory Pattern in JavaScript to get rid of a switch
statement when displaying React components.
Then I ran into a dynamic import().
I will show you 3 ways to use import()
to dynamically load React Components.
- Loading React Components Dynamically – Demo – Source Code
- Handling Different Data Types – Demo – Source Code
- Loading Components on Demand – Demo – Source Code
Let’s dive in 🏊~
Case 1 – Loading React Components Dynamically
Problem
Suppose that you want to display differently for each event in an events array.
const events = [ | |
"PushEvent", | |
"PushEvent", | |
"PushEvent", | |
"ReleaseEvent", | |
"StatusEvent", | |
"BadEvent" | |
]; | |
ReactDOM.render(<IfApp events={events} />, document.getElementById("root")); |
Within IfApp.render()
, one would use a if/switch
statement to check what type of event it is and create a component as shown below.
import React, { Component } from "react"; | |
import shortid from "shortid"; | |
import PushEvent from './components/PushEvent'; | |
import ReleaseEvent from './components/ReleaseEvent'; | |
import StatusEvent from './components/StatusEvent'; | |
import "./App.css"; | |
class IfApp extends Component { | |
render() { | |
const {events} = this.props; | |
const eventElement = events.map(event => { | |
switch (event) { | |
case "PushEvent": return <PushEvent key={shortid.generate()} />; | |
case "ReleaseEvent": return <ReleaseEvent key={shortid.generate()} />; | |
case "StatusEvent": return <StatusEvent key={shortid.generate()} />; | |
default: return <div key={shortid.generate()}></div>; | |
} | |
}); | |
return ( | |
<div> | |
{eventElement} | |
</div> | |
); | |
} | |
} | |
export default IfApp; |
There are two issues with this approach.
- it quickly becomes a maintenance nightmare as new event gets created.
- We are importing components even if we don’t need it
Alternative Approach
We can mitigate the problem if
- We can import dynamically only the components we need, and
- also make it easier to maintain by getting rid of switch statement.
I will use import()
statement to dynamically load event component (Please refer to this excellent article by Serg Hospodarets for import()
).
Here is the demo code structure.
Here is the method to add a component by type name (“PushEvent”, “ReleaseEvent”, and “StatusEvent”).
addComponent = async type => { | |
console.log(`Loading ${type} component...`); | |
import(`./components/${type}.js`) | |
.then(component => | |
this.setState({ | |
components: this.state.components.concat(component.default) | |
}) | |
) | |
.catch(error => { | |
console.error(`"${type}" not yet supported`); | |
}); | |
}; |
Given a type name, addComponent()
imports a component file and adds it to this.state.components
.
And also, if an unknown type is passed, it displays an error message in console.
And the method is called for each type within componentDidMount()
.
async componentDidMount() { | |
const { events } = this.props; | |
events.map(async type => await this.addComponent(type)); | |
} |
We render imported components as shown below.
render() { | |
const { components } = this.state; | |
if (components.length === 0) return <div>Loading...</div>; | |
const componentsElements = components.map(Component => ( | |
<Component key={shortid.generate()} /> | |
)); | |
return <div className="App">{componentsElements}</div>; | |
} |
Note that you need to have a unique key for each Component
object instance, so I used shortid to generate unique key for each component.
The full source for App
component is shown below.
import React, { Component } from "react"; | |
import shortid from "shortid"; | |
import "./App.css"; | |
class App extends Component { | |
constructor(props) { | |
super(props); | |
this.state = { | |
components: [] | |
}; | |
} | |
addComponent = async type => { | |
console.log(`Loading ${type} component...`); | |
import(`./components/${type}.js`) | |
.then(component => | |
this.setState({ | |
components: this.state.components.concat(component.default) | |
}) | |
) | |
.catch(error => { | |
console.error(`"${type}" not yet supported`); | |
}); | |
}; | |
async componentDidMount() { | |
const { events } = this.props; | |
events.map(async type => await this.addComponent(type)); | |
} | |
render() { | |
const { components } = this.state; | |
if (components.length === 0) return <div>Loading...</div>; | |
const componentsElements = components.map(Component => ( | |
<Component key={shortid.generate()} /> | |
)); | |
return <div className="App">{componentsElements}</div>; | |
} | |
} | |
export default App; |
Now the switch
statement within render()
is gone and App
doesn’t need to change when a new type is added (refer to Open-Close Principle). When a new type is added, we just need to create a new component under components
folder.
And also, scripts get loaded dynamically as shown in the video below.
Live Demo on Netlify
Case 2 – Handling Different Data Types
Let’s take a look at more advanced scenario. Now each type is associated with data having different schema.
[ | |
{ | |
"type": "PushEvent", | |
"actor": { | |
}, | |
"repo": { | |
}, | |
"payload": { | |
"push_id": 1111, | |
"commits": [ | |
{ | |
"sha": "...", | |
"author": {}, | |
"message": "...", | |
"distinct": true, | |
"url": "https://api.github.com/repos/..." | |
} | |
] | |
}, | |
}, | |
... | |
{ | |
"type": "CreateEvent", | |
"actor": { | |
}, | |
"repo": { | |
}, | |
"payload": { | |
"ref": "master", | |
"ref_type": "branch", | |
"pusher_type": "user" | |
}, | |
}, | |
{ | |
"type": "WatchEvent", | |
"actor": { | |
}, | |
"repo": { | |
}, | |
"payload": { | |
"action": "started" | |
}, | |
}, | |
] |
It’s from an actual response from a public GitHub API call.
There are 37 types of events, which one having slightly different response schema.
As it was the case in the Case 1, we could simply create an GitHub event handler component and let each one deal with different type of payload data.
First, the code structure looks like this.
We pass the JSON response to GitHubEventApp
like following.
fetch(`https://api.github.com/users/codingblocks/events/public`) | |
.then(response => response.json()) | |
.then(events => { | |
console.log("index.js events", events); | |
ReactDOM.render( | |
<GitHubEventApp events={events} />, | |
document.getElementById("root") | |
); | |
}); |
Then we load components for each event in componentDidMount()
.
async componentDidMount() { | |
const { events } = this.props; | |
events.map(async event => await this.addComponent(event)); | |
} | |
addComponent = async event => { | |
const { type } = event; | |
console.log(`Loading ${type} component...`); | |
import(`./github_components/${type}.js`) | |
.then(Component => | |
this.setState({ | |
components: this.state.components.concat( | |
<Component.default key={shortid.generate()} {...event} /> | |
) | |
}) | |
) | |
.catch(error => { | |
// Add an empty component for loading message | |
this.setState({ | |
components: this.state.components.concat( | |
<NullComponent key={shortid.generate()} /> | |
) | |
}) | |
console.error(`"${type}" not yet supported`); | |
}); | |
}; |
Two things are worth mentioning here;
- I am instantiating
Component
withinthis.setState
. It is because to make component sorting easier later inrender()
(I’d appreciate it if anyone let me know instantiating here instead of inrender()
would cause a performance issue or not). - I am passing all event properties while instantiating a component (Each dynamically imported components can pick and choose props to use).
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
<Component.default key={shortid.generate()} {...event} />
Now let’s render the result in descending order (higher the ID, the later the event was created).
render() { | |
const { components } = this.state; | |
const { events } = this.props; | |
if (components.length !== events.length) return <div>Loading...</div>; | |
components.sort((c1, c2) => c2.props.id - c1.props.id); | |
return <div className="App">{components}</div>; | |
} |
Here is the glorious result 🎉🎉🎉 (Please pardon the appearance…)
You can see that each event are rendered differently.
Component codes are listed here for the sake of completeness.
Live Demo on Netlify
ForkEvent.js
import React from "react"; | |
const ForkEvent = ({ created_at: eventDate, repo, org, actor, payload }) => { | |
const { display_login: login, url: actorURL } = actor; | |
const { name: repoName, url: repoURL } = repo; | |
return ( | |
<div> | |
<h3>Fork - (<small>{eventDate.toString()}</small>)</h3> | |
<p> | |
<a href={actorURL}>{login}</a> has forked{" "} | |
<a href={repoURL}>{repoName}</a> | |
</p> | |
</div> | |
); | |
}; | |
export default ForkEvent; |
NullEvent.js
import React from "react"; | |
// Null object pattern | |
const NullEvent = () => { | |
return ( | |
<div></div> | |
); | |
}; | |
export default NullEvent; |
PushEvent.js
import React from "react"; | |
import shortid from 'shortid'; | |
const PushEvent = ({ created_at: eventDate, repo, org, actor, payload }) => { | |
const { display_login: login, url: actorURL } = actor; | |
const { commits } = payload; | |
const { name: repoName, url: repoURL } = repo; | |
return ( | |
<div> | |
<h3>Push - (<small>{eventDate}</small>)</h3> | |
<div> | |
<a href={actorURL}>{login}</a> has pushed to{" "} | |
<a href={repoURL}>{repoName}</a> | |
<Commits commits={commits} /> | |
</div> | |
</div> | |
); | |
}; | |
const Commits = ({commits}) => { | |
return ( | |
<ul> | |
{commits.map(commit => ( | |
<li key={shortid.generate()}>{commit.author.name} has commited with message "{commit.message}"</li> | |
))} | |
</ul> | |
); | |
}; | |
export default PushEvent; |
WatchEvent.js
import React from "react"; | |
const WatchEvent = ({ created_at: eventDate, repo, org, actor, payload }) => { | |
const { display_login: login, url: actorURL } = actor; | |
const { action } = payload; | |
const { name: repoName, url: repoURL } = repo; | |
return ( | |
<div> | |
<h3>Watch - (<small>{eventDate.toString()}</small>)</h3> | |
<p> | |
<a href={actorURL}>{login}</a> has {action} following{" "} | |
<a href={repoURL}>{repoName}</a> | |
</p> | |
</div> | |
); | |
}; | |
export default WatchEvent; |
Case 3 – Loading Components on Demand
The last case is when we want to display different views for the same input.
Suppose that given data, you can show it as a tabular form or as a graph.
Code structure looks like this.
Here is the shared data we want to display in tabular and/or graph representations.
// https://frappe.github.io/charts/ | |
const data = { | |
labels: [ | |
"12am-3am", | |
"3am-6am", | |
"6am-9am", | |
"9am-12pm", | |
"12pm-3pm", | |
"3pm-6pm", | |
"6pm-9pm", | |
"9pm-12am" | |
], | |
datasets: [ | |
{ | |
title: "Some Data", | |
values: [25, 40, 30, 35, 8, 52, 17, -4] | |
}, | |
{ | |
title: "Another Set", | |
values: [25, 50, -10, 15, 18, 32, 27, 14] | |
}, | |
{ | |
title: "Yet Another", | |
values: [15, 20, -3, -15, 58, 12, -17, 37] | |
} | |
] | |
}; |
Here is how the result looks before digging into the implementation (Please pardon my CSS skills again).
Live Demo on Netlify
App
component initializes state with following properties.
this.state = { | |
loadedComponents: [], | |
components: [] | |
}; |
-
loadedComponents
tracks what components have been added as not to load more than once. -
components
holds view components (tabular or graph).
render()
simply has 3 buttons and handles the click event.
render() { | |
const { components } = this.state; | |
return ( | |
<div className="App"> | |
<div className="buttons"> | |
<div> | |
<button id="table" onClick={this.handleShowTableChange}>Show Table</button> | |
</div> | |
<div> | |
<button id="graph" onClick={this.handleShowGraphChange}>Show Graph</button> | |
</div> | |
<div> | |
<button id="null" onClick={this.handleNullGraphChange}>Not yet implemented...</button> | |
</div> | |
</div> | |
<div className="views"> | |
{components.length === 0 ? ( | |
<div>Nothing to display...</div> | |
) : (components)} | |
</div> | |
</div> | |
); | |
} |
Each onClick
events handlers adds a different view.
handleShowTableChange = async event => { | |
await this.addView("TableView"); | |
}; | |
handleShowGraphChange = async event => { | |
await this.addView("GraphView"); | |
}; | |
handleNullGraphChange = async event => { | |
await this.addView("NullView"); | |
}; |
addView
imports a new view component by view name.
addView = async viewName => { | |
// Don't load more than once. | |
if (this.state.loadedComponents.includes(viewName)) return; | |
console.log(`Loading ${viewName} view...`); | |
import(`./views/${viewName}.js`) | |
.then(Component => { | |
this.setState({ | |
loadedComponents: this.state.loadedComponents.concat(viewName), | |
components: this.state.components.concat( | |
<Component.default | |
key={shortid.generate()} | |
data={this.props.data} | |
/> | |
) | |
}); | |
}) | |
.catch(error => { | |
this.setState({ | |
loadedComponents: this.state.loadedComponents.concat(viewName), | |
components: this.state.components.concat( | |
<NullView key={shortid.generate()} /> | |
) | |
}); | |
}); | |
}; |
Here are the views components.
TableView.js
– Formats data using HTML table.
import React from "react"; | |
import shortid from "shortid"; | |
const TableView = ({ data }) => { | |
const { labels, datasets } = data; | |
const headers = datasets.map(set => ( | |
<th key={shortid.generate()}>{set.title}</th> | |
)); | |
const rows = labels.map((label, i) => { | |
return ( | |
<tr key={shortid.generate()}> | |
<td key={shortid.generate()}>{label}</td> | |
{datasets.map(set => <td key={shortid.generate()}>{set.values[i]}</td>)} | |
</tr> | |
); | |
}); | |
return ( | |
<table> | |
<thead> | |
<tr> | |
<th key={shortid.generate()}>Labels</th> | |
{headers} | |
</tr> | |
</thead> | |
<tbody>{rows}</tbody> | |
</table> | |
); | |
}; | |
export default TableView; |
GraphView.js
– Formats data graphically.
import React from "react"; | |
import Chart from "./Chart"; | |
import "frappe-charts/dist/frappe-charts.min.css"; | |
// https://github.com/tobiaslins/frappe-charts-react-example | |
const GraphView = ({ data }) => <Chart title="Graph data" data={data} />; | |
export default GraphView; |
NullView.js
– Does nothing.
import React from 'react'; | |
const NullView = () => <div>This View is not yet implemented...</div>; | |
export default NullView; |
Parting Words
I’ve discussed three cases.
- Loading React Components Dynamically – Demo – Source Code
- Handling Different Data Types – Demo – Source Code
- Loading Components on Demand – Demo – Source Code
I’d appreciate it if you can point out any optimizations or improvements I can make.
The post Loading React Components Dynamically on Demand appeared first on Slight Edge Coder.
Top comments (0)