When I first came across Spectacle, I liked how easy it was to get started and that it’s built with React. I write React code on a daily basis and why not use it to create presentations? All went well for a while until I realized that the ./presentation/index.js file started to exceed a few hundred lines.
There must be a way to modularize my slides!
tl;dr: The code is available at https://github.com/mikenikles/presentations/tree/master/packages/blog-post-source
As recommended in the Spectacle Github repo, I started by cloning the spectacle-boilerplate repo and removed the .git folder:
$ git clone [email protected]:FormidableLabs/spectacle-boilerplate.git blog-post-source$ cd blog-post-source$ rm -fr .git
At this point, you can install dependencies with npm install
and start the presentation with npm start
. It will be available at http://localhost:3000.
There are four slides in the boilerplate presentation. Let’s create a ./presentation/slides directory where we’ll move each slide in its own subfolder.
$ cd ./presentation$ mkdir slides && cd slides$ mkdir 1 && mkdir 2 && mkdir 3 && mkdir 4
We also want a index.js file in each folder. This is where each slide’s content is going to be.
$ touch ./1/index.js && touch ./2/index.js && touch ./3/index.js && touch ./4/index.js
This leaves us with the following directory structure:
Commit: https://github.com/mikenikles/presentations/commit/5d208f669c633da7d95424d48e62588081bd7d56
The spectacle-boilerplate repo already provides each slide’s content in ./presentation/index.js. All we need to do is move each <Slide />
React component into its corresponding ./presentation/slides/[slide-number]/index.js file.
Let’s do that together for the first slide.
Cut and paste that to ./presentation/slides/1/index.js
We also have to add a few import statements to ./presentation/slides/1/index.js. Also, let’s make sure we export the code for this slide. The final file looks like this:
import React from "react";import { Heading, Slide, Text } from "spectacle";
export default (<Slide transition={["zoom"]} bgColor="primary"><Heading size={1} fit caps lineHeight={1} textColor="secondary">Spectacle Boilerplate</Heading><Text margin="10px 0 0" textColor="tertiary" size={1} fit bold>open the presentation/index.js file to get started</Text></Slide>);
Follow the same steps for the remaining slides.
Commit: https://github.com/mikenikles/presentations/commit/a45f144247d0e4f0c39d922a9a23cf73c05c0a32
Lastly, we have to load each slide dynamically. This sounds trickier than it is. At a high-level, the following steps are required:
import()
.Presentation
component’s state.key
prop to each dynamically loaded slide.import()
In ./presentation/index.js, we define a list of all slides and their order.
const slidesImports = [import("./slides/1"),import("./slides/2"),import("./slides/3"),import("./slides/4")];
Each import()
statement returns a promise. So slidesImports
is an array of Promises. We can leverage that and use the Promise.all()
function to wait until all slides have been imported. More on that shortly.
Presentation
component’s state.The Presentation
component needs a state
where we provide the loaded slides once they’re available. We populate an empty array in the constructor()
and replace it with the actual slides’ content in the componentDidMount()
lifecycle method. The new Presentation
component now looks like this:
export default class Presentation extends React.Component {constructor(props) {super(props);
this.state = {
slides: \[\] // A placeholder for slides once they're loaded.
};
}
componentDidMount() {const importedSlides = [];Promise.all(slidesImports).then((slidesImportsResolved) => {slidesImportsResolved.forEach((slide) => {importedSlides.push(slide.default);});this.setState({ slides: importedSlides });});}
render() {return (<Deck transition={["zoom", "slide"]} transitionDuration={500} theme={theme}></Deck>);}}
We’re almost done. Next up, let’s update the render()
function and actually render all slides.
render() {const { slides } = this.state;return (<Deck transition={["zoom", "slide"]} transitionDuration={500} theme={theme}>{slides.map((slide) => {return slide;})}</Deck>);}
When we look at http://localhost:3000/, we see a blank screen and the following error in the browser console:
Browser console error based on the current code
A closer look at manager.js on line 415 reveals the error is caused by the following line of code:
children: _react.Children.toArray(child.props.children),
Based on the error message, we know that child
is undefined
. That’s an easy fix.
When the Presentation
component’s render()
function is called for the first time, this.state.slides
is set to an empty array. Spectacle doesn’t like that, so let’s provide some placeholder slides until our real slides are imported and added to the state.
We could provide an empty slide until this.state.slides
is available, along the lines of:
render() {const { slides } = this.state;return (<Deck transition={["zoom", "slide"]} transitionDuration={500} theme={theme}>{slides.length ? slides.map((slide) => {return slide;}) : <Slide />}</Deck>);}
That actually works fine when we load the first slide at http://localhost:3000/. However, try to navigate to the second slide and reload the page at http://localhost:3000/#/1. Error.
What this teaches us is that Spectacle needs to know the exact number of slides a presentation requires upfront upon first calling the render()
function of the Presentation
component.
Easy, let’s make it happen by changing the constructor()
function from:
constructor(props) {super(props);
this.state = {slides: []};}
to:
constructor(props) {super(props);
this.state = {slides: Array(slidesImports.length).fill(<Slide key="loading" />)};}
We basically populate the slides
property of the state with the exact number of slides that we’ll import. In the code snippet above, we render an empty <Slide />
component, but we could just as well design a nice slide that displays a “Loading…” spinner.
Now head back to http://localhost:3000/#/1 and enjoy the second slide of your presentation without the nasty error we saw earlier.
Wait a minute, I spoke too soon…
Each <Slide /> component requires a unique “key” prop
Oh yeah, that’s right. The link in the error message explains why that key
prop is important.
key
prop to each dynamically loaded slideWe have two options to do that:
key
prop to each <Slide />
component within ./presentation/slides/[slide-number]/index.js.key
prop dynamically in the Presentation
component’s render()
function.Option 1 sounds simple, but we’ll go for option 2 because it makes it easier to rearrange slides later. If the individual slide index.js
files are unaware of their position within the presentation, we can simply rename the slide’s folder from 1
to 3
to move a slide from the first to the third position in the presentation.
The render()
function’s updated <Deck />
component now looks like this:
<Deck transition={["zoom", "slide"]} transitionDuration={500} theme={theme}>{slides.map((slide, index) => {return React.cloneElement(slide, {key: index});})}</Deck>
Commit: https://github.com/mikenikles/presentations/commit/2c8630086548405e7d7ac2394d087fcfe504b06c
With this approach, I can now easily modularize my Spectacle presentations. In fact, I can take this to a whole new level…
I could create a NPM module with a collection of commonly used slides, such as an “About Me” slide I use for every presentation. Whenever I want to use that in a presentation, I could simply add it as a dependency to my package.json file and import it into my presentation at the correct index in my deck.
If you have any questions, don’t hesitate to reach out!