actionhero javascript react
2019-12-02T00:54:12.808Z
↞ See all posts
I recently moved the Actionhero tutorials from the Actionhero Docs site docs.actionherojs.com to the main Actionhero website www.actionherojs.com. We are switching Actionhero from Javascript to Typescript, and as such we’ve changed from using JSDoc to TypeDoc to generate our documentation site. Previously, we had a custom "theme" for JSdoc which included our Tutorials within the docs, but that was a bit of a hack. To me, there’s a distinction between `tutorials` and `docs`, and having both in the same place could lead to confusion. This was a great time to make the switch.
I think to have a well-documented project you need both of these components — Docs and Tutorials, but they aren’t consumed by the same audience in the same way.
I often reference this wonderful guide by Divio talking about the different types of documentation: https://www.divio.com/blog/documentation/. You should read it if you aren’t familiar with the "Cooking" metaphor for documentation.
Markdown in your HTML
It was very pleasant to write Actionhero’s tutorials in Markdown. It makes focusing on the content rather than the style very simple, while abstracting away all the DIVs and TAGs of HTML. It also makes it easy to Diff changes when updating the site (i.e. when looking at a Pull Request). With the goal of keeping this part of the site in Markdown, we needed to find a way to render it React.
The React Markdown package is wonderful at this step. You can load in a Markdown file and React Markdown with generate the HTML.
A few tips:
componentDidMount
stage of the lifecycle. This may have adverse effects on the SEO of those pages.getInitialProps
! This means that the markdown content will be passed down from the server on initial page load.1export default class ToutorialPage extends Component<Props, State> { 2 static async getInitialProps(ctx) { 3 const name = ctx.query.name; 4 const markdown = await require(`./../../tutorials/${name}.md`); 5 return { 6 markdown: markdown.default, 7 name, 8 }; 9 } 10 render() { 11 return ( 12 <ReactMarkdown 13 source={this.props.markdown} 14 escapeHtml={false} 15 renderers={{}} 16 /> 17 ); 18 } 19}
In the example above you can see that react-markdown lets us provide special renderers for each HTML element. 2 things that were important to this project were rendering code properly, and adding sub-navigation to each page.
Adding code was easy, as we already had a component for rendering code based on react-syntax-highlighter.
1import { Component } from "react"; 2import SyntaxHighlighter from "react-syntax-highlighter"; 3import { docco } from "react-syntax-highlighter/dist/cjs/styles/hljs"; 4interface Props { 5 language?: string; 6 showLineNumbers?: boolean; 7 value?: string; 8} 9export default class extends Component<Props> { 10 render() { 11 const language = this.props.language || "typescript"; 12 const showLineNumbers = this.props.showLineNumbers || false; 13 return ( 14 <SyntaxHighlighter 15 language={language} 16 style={docco} 17 showLineNumbers={showLineNumbers} 18 > 19 {this.props.value ? this.props.value : this.props.children} 20 </SyntaxHighlighter> 21 ); 22 } 23}
We just pass that component into our example above:
1import Code from "./../../components/code"; 2export default class ToutorialPage extends Component<Props, State> { 3 static async getInitialProps(ctx) { 4 const name = ctx.query.name; 5 const markdown = await require(`./../../tutorials/${name}.md`); 6 return { 7 markdown: markdown.default, 8 name, 9 }; 10 } 11 render() { 12 return ( 13 <ReactMarkdown 14 source={this.props.markdown} 15 escapeHtml={false} 16 renderers={{ 17 code: Code, // <-- HERE 18 }} 19 /> 20 ); 21 } 22}
Adding navigation was a bit tricker. We accomplished this by creating a custom renderer for Headers that also built up a list of all the section headers into the page’s state
with this new parseHeading
method:
1import Code from "./../../components/code"; 2export default class ToutorialPage extends Component<Props, State> { 3 static async getInitialProps(ctx) { 4 const name = ctx.query.name; 5 const markdown = await require(`./../../tutorials/${name}.md`); 6 return { 7 markdown: markdown.default, 8 name, 9 }; 10 } 11 render() { 12 return ( 13 <ReactMarkdown 14 source={this.props.markdown} 15 escapeHtml={false} 16 renderers={{ 17 code: Code, // <-- HERE 18 }} 19 /> 20 ); 21 } 22}
this.state.sectionHeadings
is built in our render as we parse the headers. We then have this available to the rest of the page to draw our side navigation!
Notes:
state
within the render method, it’s easy to get into an infinite loop. That’s why we need to only modify the list of headers (sectionHeadings
) if the header isn’t present.You can read more about Actionhero’s move to Typescript in the new `Typescript` Tutorial here: https://www.actionherojs.com/tutorials/typescript (yes, it’s written in markdown)!
I write about Technology, Software, and Startups. I use my Product Management, Software Engineering, and Leadership skills to build teams that create world-class digital products.
Get in touch