Presentations With Spectacle — How I Modularize My Deck

Written by mikenikles | Published 2017/01/28
Tech Story Tags: javascript | react | spectacles

TLDRvia the TL;DR App

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

Get Started With `spectacle-boilerplate`

As recommended in the Spectacle Github repo, I started by cloning the spectacle-boilerplate repo and removed the .git folder:

$ git clone git@github.com: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.

Create a Folder For Each Slide

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

Populate Each Slide’s Content

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

Load Slides Dynamically

Lastly, we have to load each slide dynamically. This sounds trickier than it is. At a high-level, the following steps are required:

  1. Load all slides dynamically with import().
  2. Provide the loaded slides to the Presentation component’s state.
  3. Populate the state with placeholder components while step 1 and 2 above are in progress.
  4. Provide a unique key prop to each dynamically loaded slide.

1. Load all slides dynamically with 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.

2. Provide the loaded slides to the 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.

3. Populate the state with placeholder components while step 1 and 2 above are in progress

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.

4. Provide a unique key prop to each dynamically loaded slide

We have two options to do that:

  1. Add a key prop to each <Slide /> component within ./presentation/slides/[slide-number]/index.js.
  2. Provide the 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

Conclusion

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!


Published by HackerNoon on 2017/01/28