I remember when I was doing freeCodeCamp, I was about to make a Markdown editor in one of the projects. So I decided to go with the Markdown editor this time, combined with React.js and TypeScript.

What you'll learn

  • Setting up React.js project with TypeScript
  • Creating a markdown editor by compiling down it to html
  • Using React hooks to create theming for the application
  • Continuous deployments through Github Actions

I am a lazy person, I think most of you are, too. So here's the code and demo link, if you directly want to see them.

Project Source Code:

{% github https://github.com/ashwamegh/react-typescript-markdown-editor no-readme %}

Project Demo: ashwamegh/react-typescript-markdown-editor

Lets start with setting up our project

1. Setting up our project with React.js & TypeScript

We all know the capabilities of TypeScript, how it can save the day for your silly mistakes. And if combined with react, they both become a great combination to power any application.

I will be using create-react-app since, it gives the TypeScript support out of the box. Go to your root directory where you want to create the project and run this command:

npx create-react-app markdown-editor --template typescript

this --template typescript flag will do all the hard work for you, setting up React.js project with TypeScript.

Later, you'll need to remove some of bootstrapped code to start creating your application.

For reference you can check this initial commit to see what has been removed: https://github.com/ashwamegh/react-typescript-markdown-editor/commit/7cc379ec0d01f3f1a07396ff2ac6c170785df57b

After you've completed initial steps, finally we'll be moving on to creating our Markdown Editor.

2. Creating Markdown Editor

Before diving into the code, let's see the folder structure for our project, which we will be developing.

├── README.md
├── package.json
├── public
|  ├── favicon.ico
|  ├── index.html
|  ├── logo192.png
|  ├── logo512.png
|  ├── manifest.json
|  └── robots.txt
├── src
|  ├── App.test.tsx
|  ├── App.tsx
|  ├── components
|  |  ├── Editor.tsx
|  |  ├── Footer.tsx
|  |  ├── Header.tsx
|  |  ├── Main.tsx
|  |  ├── Preview.tsx
|  |  └── shared
|  |     └── index.tsx
|  ├── index.css
|  ├── index.tsx
|  ├── react-app-env.d.ts
|  ├── serviceWorker.ts
|  ├── setupTests.ts
|  └── userDarkMode.js
├── tsconfig.json
└── yarn.lock

I will be using emotion for creating styles for my components and react-icons for icons used in the project. So you'll be needed to install emotion and react-icons by running this command:

npm i -S @emotion/core @emotion/styled react-icons

or if you're using yarn like me, you can run

yarn add @emotion/core @emotion/styled react-icons

Moving forward, first of we will create a shared components folder to create components we will be reusing.

/* src/components/shared/index.tsx */

import React from 'react'
import styled from '@emotion/styled'

export const ColumnFlex = styled.div`
  display: flex;
  flex-direction: column;
`
export const RowFlex = styled.div`
  display: flex;
  flex-direction: row;
`

In this file, we have declared two styled components for flex-column and flex-row styled divs which we'll be using later. To know more about styled-components with emotion library, head on to this link.

3 Using React hooks to create a custom theme hook

We'll use react hooks to create our custom hook to implement basic theming capabilities, using which we can toggle our theme from light to dark colors.

/* useDarMode.js */

import { useEffect, useState } from 'react'

export default () => {
  const [theme, setTheme] = useState('light')

  const toggleTheme = () => {
    if (theme === 'dark') {
      setTheme('light')
    } else {
      setTheme('dark')
    }
  }

  useEffect(() => {
    const localTheme = localStorage.getItem('theme')
    if (localTheme) {
      setTheme(localTheme)
    }
  }, [])

  return {
    theme,
    toggleTheme,
  }
}

In our hooks file, we are setting the initial state of the theme to be light using useState hook. And using useEffect to check whether any theme item exists in our browser's local storage, and if there is one, pick the theme from there and set it for our application.

Since, we have defined our shared components and custom react hook for theming, let's dive into our app components.

So, I have divided our app structure into 5 components and those are: Header, Main (contains main section of the app with Editor & Preview component) and Footer component.

  1. Header // contains normal header code and a switch to toggle theme
  2. Main // container for Editor and Preview components i. Editor // contains code for Editor ii. Preview // contains code for previewing markdown code into HTML
  3. Footer // contains normal footer code
/* src/components/Header.tsx */

import React from 'react'
import { FiSun } from 'react-icons/fi'
import { FaMoon } from 'react-icons/fa'

// this comment tells babel to convert jsx to calls to a function called jsx instead of React.createElement
/** @jsx jsx */
import { css, jsx } from '@emotion/core'

// Prop check in typescript
interface Props {
  toggleTheme: () => void
  theme: string
}

const Header: React.FC<Props> = ({ theme, toggleTheme }) => {
  return (
    <header
      css={
        theme === 'dark'
          ? css`
              display: flex;
              flex-direction: row;
              justify-content: space-between;
              background-color: #f89541;
              padding: 24px 32px;
              font-size: 16px;
            `
          : css`
              display: flex;
              flex-direction: row;
              justify-content: space-between;
              background-color: #f8f541;
              padding: 24px 32px;
              box-shadow: 0px -2px 8px #000;
              font-size: 16px;
            `
      }
    >
      <div className="header-title">Markdown Editor</div>
      <div
        css={css`
          cursor: pointer;
        `}
        onClick={toggleTheme}
      >
        {theme === 'dark' ? <FaMoon /> : <FiSun />}
      </div>
    </header>
  )
}

export default Header

In this component, we are using TypeScript for prop checks and you may wonder, why we're mentioning React.FC here. Its just that, by typing our component as an FC (FunctionComponent), the React TypeScripts types allow us to handle children and defaultProps correctly.

For styling our components we're using css prop with string styles from emotion library, you can learn more about this by following the docs here

After creating the Header component, we'll create our Footer component and then we'll move on to Main component.

Let's see the code for Footer component

import React from 'react'

// this comment tells babel to convert jsx to calls to a function called jsx instead of React.createElement
/** @jsx jsx */
import { css, jsx } from '@emotion/core'

const Footer: React.FC = () => {
  return (
    <footer>
      <div
        className="footer-description"
        css={css`
          padding: 16px 0px;
          overflow: hidden;
          position: absolute;
          width: 100%;
          text-align: center;
          bottom: 0px;
          color: #f89541;
          background: #000;
        `}
      >
        <span>{`</>`}</span>
        <span>
          {' '}
          with <a href="https://reactjs.org" target="_blank">
            React.js
          </a> &amp; <a href="https://www.typescriptlang.org/" target="_blank">
            TypeScript
          </a>
        </span>
      </div>
    </footer>
  )
}

export default Footer

Footer component contains simple code to render usual credit stuff.

/* src/components/Main.tsx */

import React, { useState } from 'react'

// this comment tells babel to convert jsx to calls to a function called jsx instead of React.createElement
/** @jsx jsx */
import { css, jsx } from '@emotion/core'
import { RowFlex } from './shared'
import Editor from './Editor'
import Preview from './Preview'

interface Props {
  theme: string
}

const Main: React.FC<Props> = ({ theme }) => {
  const [markdownContent, setMarkdownContent] = useState<string>(`
# H1
## H2
### H3
#### H4
##### H5

__bold__
**bold**
_italic_
`)
  return (
    <RowFlex
      css={css`
        padding: 32px;
        padding-top: 0px;
        height: calc(100vh - 170px);
      `}
    >
      <Editor
        theme={theme}
        markdownContent={markdownContent}
        setMarkdownContent={setMarkdownContent}
      />
      <Preview theme={theme} markdownContent={markdownContent} />
    </RowFlex>
  )
}

export default Main

Since, some of the code will look familiar to you from the previous components which you can now understand yourself. Other than that, we have used useState hook to create a state to hold our markdown content and a handler to set it, called setMarkdownContent in the code.

We need to pass these down to our Editor and Preview components, so that, they can provide the user the way to edit and preview their markdown content. We have also set an initial state for content with some basic markdown text.

Let's see the code for Editor component:

/* src/components/Editor.tsx */

import React, { ChangeEvent } from 'react'
import PropTypes from 'prop-types'

// this comment tells babel to convert jsx to calls to a function called jsx instead of React.createElement
/** @jsx jsx */
import { css, jsx } from '@emotion/core'
import { ColumnFlex } from './shared'

interface Props {
  markdownContent: string
  setMarkdownContent: (value: string) => void
  theme: string
}

const Editor: React.FC<Props> = ({
  markdownContent,
  setMarkdownContent,
  theme,
}) => {
  return (
    <ColumnFlex
      id="editor"
      css={css`
        flex: 1;
        padding: 16px;
      `}
    >
      <h2>Editor</h2>
      <textarea
        onChange={(e: ChangeEvent<HTMLTextAreaElement>) =>
          setMarkdownContent(e.target.value)
        }
        css={
          theme === 'dark'
            ? css`
                height: 100%;
                border-radius: 4px;
                border: none;
                box-shadow: 0 -2px 10px rgba(0, 0, 0, 1);
                background: #000;
                color: #fff;
                font-size: 100%;
                line-height: inherit;
                padding: 8px 16px;
                resize: none;
                overflow: auto;
                &:focus {
                  outline: none;
                }
              `
            : css`
                height: 100%;
                border-radius: 4px;
                border: none;
                box-shadow: 2px 2px 10px #999;
                font-size: 100%;
                line-height: inherit;
                padding: 8px 16px;
                resize: none;
                overflow: auto;
                &:focus {
                  outline: none;
                }
              `
        }
        rows={9}
        value={markdownContent}
      />
    </ColumnFlex>
  )
}

Editor.propTypes = {
  markdownContent: PropTypes.string.isRequired,
  setMarkdownContent: PropTypes.func.isRequired,
}

export default Editor

This is a straight forward component which uses <textarea/> to provide the user a way to enter their inputs, which have to be further compiled down to render it as HTML content in the Preview component.

Now, we have created almost all the components to hold our code except the Preview component. We'll need something to compile down the user's markdown content to simple HTML, and we don't want to write all the compiler code, because we have plenty of options to choose from.

In this application, we'll be using marked library to compile down our markdown content to HTML. So you will be need to install that, by running this command:

npm i -S marked

or with yarn

yarn add marked

If you want to know more about this library, you can see it here

Let's see the code for our Preview component

/* src/components/Preview.tsx */

import React from 'react'
import PropTypes from 'prop-types'
import marked from 'marked'

// this comment tells babel to convert jsx to calls to a function called jsx instead of React.createElement
/** @jsx jsx */
import { css, jsx } from '@emotion/core'
import { ColumnFlex } from './shared'

interface Props {
  markdownContent: string
  theme: string
}

const Preview: React.FC<Props> = ({ markdownContent, theme }) => {
  const mardownFormattedContent = marked(markdownContent)

  return (
    <ColumnFlex
      id="preview"
      css={css`
        flex: 1;
        padding: 16px;
      `}
    >
      <h2>Preview</h2>
      <div
        css={
          theme === 'dark'
            ? css`
                height: 100%;
                border-radius: 4px;
                border: none;
                box-shadow: 0 -2px 10px rgba(0, 0, 0, 1);
                font-size: 100%;
                line-height: inherit;
                overflow: auto;
                background: #000;
                padding: 8px 16px;
                color: #fff;
              `
            : css`
                height: 100%;
                border-radius: 4px;
                border: none;
                box-shadow: 2px 2px 10px #999;
                font-size: 100%;
                line-height: inherit;
                overflow: auto;
                background: #fff;
                padding: 8px 16px;
                color: #000;
              `
        }
        dangerouslySetInnerHTML={{ __html: mardownFormattedContent }}
      ></div>
    </ColumnFlex>
  )
}

Preview.propTypes = {
  markdownContent: PropTypes.string.isRequired,
}

export default Preview

In this component, we are compiling the markdown content and storing it in mardownFormattedContent variable. And to show a preview of the content in HTML, we will have to use dangerouslySetInnerHTML prop to display the HTML content directly into our DOM, which we are doing by adding this dangerouslySetInnerHTML={{__html: mardownFormattedContent}} prop for the div element.

Finally we'are ready with all the component which will be need to create our Markdown editor application. Let's bring all of them in our App.tsx file.

/* src/App.tsx */

import React from 'react'
import { css, jsx } from '@emotion/core'

// Components
import Header from './components/Header'
import Main from './components/Main'
import Footer from './components/Footer'
import useDarkMode from './userDarkMode'

function App() {
  const { theme, toggleTheme } = useDarkMode()
  const themeStyles =
    theme === 'light'
      ? {
          backgroundColor: '#eee',
          color: '#000',
        }
      : {
          backgroundColor: '#171616',
          color: '#fff',
        }
  return (
    <div className="App" style={themeStyles}>
      <Header theme={theme} toggleTheme={toggleTheme} />
      <Main theme={theme} />
      <Footer />
    </div>
  )
}

export default App

In our App component, we are importing the child components and passing down the theme props.

Now, if you have followed all steps above, you'll have a running markdown editor application, for styles I have used, you can see my source code using then link I mentioned.

Now, its time to create Github actions for our project to create continuous deployment workflow on every push to master.

4 Setting up continuous deployments through Github Actions

We’ll be using Github actions workflow to build and deploy our web application on every push to master.

Since this is not a enterprise application that holds the branches for production and development, I will setup my workflow for master branch, but if in any time in future, you require to setup the Github action workflow for your enterprise application, Just be careful with the branches.

To do so, we’ll follow some steps:

  1. Create a folder in our project root directory .github/workflows/, this will hold all the workflows config.
  2. We’ll be using JamesIves/github-pages-deploy-action action to deploy our application.
  3. Next we’ll create our .yml file here, which will be responsible for the action to build and deploy our application to GitHub pages. Let’s name it build-and-deploy-to-gh-pages.yml

Let's see what goes inside this build-and-deploy-to-gh-pages.yml

# build-and-deploy-to-gh-pages.yml

name: Build & deploy to GitHub Pages
on:
  push:
    branches:
      - master
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v1
      - name: Set up Node
        uses: actions/setup-node@v1
        with:
          node-version: 10.x
      - name: Set email
        run: git config --global user.email "${{ secrets.adminemail }}"
      - name: Set username
        run: git config --global user.name "${{ secrets.adminname }}"
      - name: npm install command
        run: npm install
      - name: Run build command
        run: npm run build
      - name: Deploy
        uses: JamesIves/github-pages-deploy-action@releases/v3
        with:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          BASE_BRANCH: master
          BRANCH: gh-pages # The branch the action should deploy to.
          FOLDER: build # The folder the action should deploy.

This workflow will run every time, we push something into master and will deploy the application through gh-pages branch.

Let's Breakdown the Workflow file

name: Build & deploy to GitHub Pages
on:
  push:
    branches:
      - master

This defines our workflow name and trigger for running the jobs inside it. Here we are setting the trigger to listen to any Push events in master branch.

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v1
      - name: Set up Node
        uses: actions/setup-node@v1
        with:
          node-version: 10.x
      - name: Set email
        run: git config --global user.email "${{ secrets.adminemail }}"
      - name: Set username
        run: git config --global user.name "${{ secrets.adminname }}"
      - name: npm install command
        run: npm install
      - name: Run build command
        run: npm run build
      - name: Deploy
        uses: JamesIves/github-pages-deploy-action@releases/v3
        with:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          BASE_BRANCH: master
          BRANCH: gh-pages # The branch the action should deploy to.
          FOLDER: build # The folder the action should deploy.

This is the most important part in our workflow, which declares the jobs to be done. Some of the lines in the config are self-explanatory runs-on: ubuntu-latest it defines the system, it will be running on.

- name: Checkout
        uses: actions/checkout@v1

This is an action for checking out a repo, and in later jobs we are setting our development environment by installing node and setting our git profile config. Then we are running npm install to pull out all the dependencies and finally running the build command.

- name: Deploy
        uses: JamesIves/github-pages-deploy-action@releases/v3
        with:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          BASE_BRANCH: master
          BRANCH: gh-pages # The branch the action should deploy to.
          FOLDER: build # The folder the action should deploy.

After the build command has been completed, we are using JamesIves/github-pages-deploy-action@releases/v3 action to deploy our build folder to gh-pages.

Whenever, you'll push something in your master branch, this workflow will run and will deploy your static build folder to gh-pages branch.

Alt Text

Now, when the deployment is completed, you'all have your app running at you github link https://yourusername.github.io/markdown-editor/.

Don't forget to add "homepage" : "https://yourusername.github.io/markdown-editor/" in package.json file, otherwise serving of static contents may cause problem.

If you liked my article, you can follow me on Twitter for my daily paper The JavaSc®ipt Showcase, also you can follow my personal projects over Github. Please post in comments, how do you like this article. Thanks!!

{% twitter 1233694229232324608 %} {% github https://github.com/ashwamegh/react-typescript-markdown-editor no-readme %}