How to build a customer-facing roadmap with React
Tags: react, javascript, saas, frontend
Published: 2021-11-29 21:01:12

A product roadmap summarizes how a product strategy leads to the actual implementation and charts out your product's vision and direction.

This article will guide you through each step you need to build a roadmap tool using React for your SaaS startup.

---

## Let's get started 🚀

📩 **Step 1: Installing React**
We'll start by creating a new react project using `create-react-app`. 

```powershell
npx create-react-app roadmap
```

 📝**Step 2: Create a roadmap component**

We'll create a component - `Roadmap` - that'll contain our display and API logic - `src/components/Roadmap`.

Create respective `Roadmap.js`, `index.js` and `Roadmap.css` files. Add following code in respective order.

```jsx
import React from "react";

import "./Roadmap.css";

function Roadmap() {
  const columnMap = [
    {
      title: "Exploring",
      tickets: [],
    },
    {
      title: "In Progress",
      tickets: [],
    },
    {
      title: "Done",
      tickets: [],
    },
    {
      title: "Leaving it for now",
      tickets: [],
    },
  ];

  return (
    <div className="roadmap">
      {columnMap.map((column, i) => (
        <div className="roadmap-column" key={`${column.title}-${i}`}>
          <div className="roadmap-column-heading">{column.title}</div>
          <div className="roadmap-cards">
            {column.tickets.map((t, i) => (
              <div key={`exploring-${i}`} className="roadmap-cards-item">
                <div className="roadmap-cards-item-heading">{t.title}</div>
                <div
                  className="roadmap-cards-item-content"
                  dangerouslySetInnerHTML={{ __html: t.description }}
                />
                <svg
                  xmlns="http://www.w3.org/2000/svg"
                  x="0px"
                  y="0px"
                  viewBox="0 0 100 125"
                  style={{
                    enableBackground: "new 0 0 100 100",
                    maxWidth: "18px",
                    cursor: "pointer",
                  }}
                >
                  <g>
                    <g>
                      <path d="M81.7,40.4H64.6l3.6-17.9c0.5-2.5,0.1-5-1.2-7.1l0,0c-1.5-2.5-4.9-3-7-1.1L29.8,38.7c-1.2,1.1-1.8,2.6-1.8,4.1v33.1    c0,2.4,1.5,4.5,3.7,5.3l12.9,3.4c3.1,1.1,6.4,1.6,9.7,1.6h18c6.6,0,12.5-4.2,14.7-10.5l6.9-20c0.3-0.9,0.4-1.9,0.4-2.9v0    C94.3,46,88.7,40.4,81.7,40.4z" />
                      <path d="M22.2,40.4H7.8c-1.5,0-2.8,1.2-2.8,2.8v34c0,1.5,1.2,2.7,2.7,2.8h14.5c1.5,0,2.8-1.2,2.8-2.8v-34    C25,41.6,23.7,40.4,22.2,40.4z" />
                    </g>
                  </g>
                </svg>
              </div>
            ))}
          </div>
        </div>
      ))}
    </div>
  );
}

export default Roadmap;
```
```jsx
export { default } from "./Roadmap";
```
```css
.roadmap {
  display: flex;
  width: 100%;
  max-width: 900px;
  min-height: 400px;
}

.roadmap > div:not(:last-child) {
  margin-right: 16px;
}

.roadmap-column {
  display: flex;
  flex-direction: column;
  flex: 1;
  background: #eef0fc;
  border-radius: 4px;
  padding: 6px 10px;
}

.roadmap-column-heading {
  color: #4d5273;
  font-size: 14px;
  font-weight: 500;
  padding: 10px;
  margin-bottom: 10px;
  text-align: center;
}

.roadmap-cards {
  display: flex;
  flex-direction: column;
}

.roadmap-cards > div:not(:last-child) {
  margin-bottom: 12px;
}

.roadmap-cards-item {
  background: #fff;
  border-radius: 4px;
  padding: 12px;
  box-shadow: 0 1px 0 #091e4240;
}
.roadmap-cards-item-heading {
  font-size: 14px;
  font-weight: 500;
}

.roadmap-cards-item-upvote {
  display: flex;
  font-size: 12px;
  line-height: 18px;
}

.roadmap-cards-item-upvote-count {
  margin-left: 6px;
}

.roadmap-cards-item p {
  font-size: 12px;
  margin-top: 7px;
  line-height: 1.5;
}

svg.not-filled path {
  fill: #fff;
  stroke: #000;
  stroke-width: 8px;
}

@media (max-width: 600px) {
  .roadmap {
    flex-direction: column;
  }

  .roadmap > div:not(:last-child) {
    margin-right: 0;
    margin-bottom: 26px;
  }
}
```

Now that our display component is created, let's app it to `App.js` and see how our component looks.


👨‍🔧**Step 3: Add component to App**
We import our component in `App.js` and remove all the unnecessary files and code. Our code - `App.js`, `App.css` and component should look like below.

Add following code to `App.js` and `App.css` respectively.

```jsx
import React from "react";

import Roadmap from "./components/Roadmap";
import "./App.css";

function App() {
  return (
    <div>
      <div className="wrapper-heading">Roadmap</div>
      <Roadmap />
    </div>
  );
}

export default App;
```
```css
@import url("https://fonts.googleapis.com/css2?family=Roboto:wght@400;500&display=swap");

html,
body {
  font-family: "Roboto", sans-serif;
}

.wrapper-heading {
  padding: 16px;
  text-align: center;
  margin-bottom: 18px;
  max-width: 900px;
  color: #4d5273;
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: 1.6px;
}
```
Now run `yarn start` in root of our project and our component should look like this.

![Roadmap](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/s0r1yx4ekj0sjumyohkx.png)
 
Now it's time to integrate this with a backend to fetch our roadmap tickets and display them. 👩‍🔧

 👩‍💻**Step 4: Get your APIs**

Follow the below-stated pointers and get your APIs! 

- **Cloning the sample project -** To get the APIs, you can visit this [link](https://app.canonic.dev/projects/617a311fe42834002d04988a/graph?utm_source=dev.to&utm_medium=organic_article&utm_campaign=30daysofcanonic_uc_1&utm_term=step4_link&utm_content=day_1) and click on the top right button to clone the project.

![cloning](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rmbfcj1wr8thvs8cbsic.png)
 

- **Deploy and get the backend URL -**  Having cloned the project, it's now time to deploy it so that we can get our APIs and backend hosted. Open the cloned project if you are not already there and you’ll see a  `Deploy` button on the top right.
    
    Click `Deploy` → Select an environment → Hit deploy.
    
    Upon completing the project, it will provide an API URL.
    
![deploy](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/unghji80mxtwbhmhwos3.png)
 

📝**Checking the documentation**
Before we get started with backend integration, let's move to the documentation by clicking on docs on the left sidebar so that we get a better understanding of the APIs involved. 

![documentation](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/f6yvnnyf3pcqm7lwmrsu.png)
 

---

### Backend integration with GraphQL 👇

Once you have your APIs ready, we can start by installing graphql. 

📩**Step 5: Install GraphQL packages**
We will need two packages for this step since we'll be using graphql to pull our data from the backend - Apollo Client and GraphQL.

```powershell
yarn add @apollo/client graphql
```

⚒**Step 6: Configure graphql** 

Configure the Apollo Client in the project directory, inside `App.js` so it would communicate with the backend.

Be sure to replace the `uri` with the one you'll get from Canonic.

```jsx
import React from "react";
import { ApolloProvider, InMemoryCache, ApolloClient } from "@apollo/client";

import Roadmap from "./components/Roadmap";

import "./App.css";

/**
 * Connecting with the backend using apollo client
 */
const client = new ApolloClient({
  // make sure you update the URI here to point to your backend
  uri: "https://roadmap-35a418.can.canonic.dev/graphql",
  cache: new InMemoryCache(),
});

function App() {
  return (
    <ApolloProvider client={client}>
      <div>
        <div className="wrapper-heading">Roadmap</div>
        <Roadmap />
      </div>
    </ApolloProvider>
  );
}

export default App;
```

👨‍🔧**Step 7: Querying the data**

We store our graphql queries inside a directory `src/gql`. Inside which we create a file and name it `queries.js`. 

This is where we will write the graphql queries for querying the data.

```jsx
import { gql } from "@apollo/client";

/**
 * gql query to get roadmap
 * The query parameters we got straight from Canonic autogenerated documentation
 */
export const GET_ROADMAP = gql`
  query {
    roadmaps {
      _id
      title
      description
      upvotes {
        count
      }
      stage {
        value
      }
    }
  }
`;
```

The next step is **Querying data and displaying our roadmap tickets.** We'll execute the graphql query in the Roadmap component and display the tickets fetched according to their status.

We modify `Roadmap.js` to achieve the above mentioned logic and get the tickets.

```jsx
import React from "react";
import { useQuery } from "@apollo/client";

import { GET_ROADMAP } from "../../gql/queries";

import "./Roadmap.css";

function Roadmap() {
  const { data = {}, loading } = useQuery(GET_ROADMAP);

  const { roadmaps = [] } = data;

  // dividing tickets into their respective categories
  const exploringTickets = roadmaps.filter(
    (t) => t.stage.value === "EXPLORING"
  );
  const inProgressTickets = roadmaps.filter(
    (t) => t.stage.value === "IN_PROGRESS"
  );
  const doneTickets = roadmaps.filter((t) => t.stage.value === "DONE");
  const leavingItForNowTickets = roadmaps.filter(
    (t) => t.stage.value === "LEAVING_IT_FOR_NOW"
  );

  const columnMap = [
    {
      title: "Exploring",
      tickets: exploringTickets,
    },
    {
      title: "In Progress",
      tickets: inProgressTickets,
    },
    {
      title: "Done",
      tickets: doneTickets,
    },
    {
      title: "Leaving it for now",
      tickets: leavingItForNowTickets,
    },
  ];

  return (
    <div className="roadmap">
      {loading ? (
        <svg
          xmlns="http://www.w3.org/2000/svg"
          style={{
            margin: "auto",
            background: "none",
            display: "block",
            shapeRendering: "auto",
            maxWidth: "30px",
            marginTop: "-20px",
          }}
          width="200px"
          height="200px"
          viewBox="0 0 100 100"
          preserveAspectRatio="xMidYMid"
        >
          <circle
            cx="50"
            cy="50"
            fill="none"
            stroke="#4d5273"
            strokeWidth="10"
            r="35"
            strokeDasharray="164.93361431346415 56.97787143782138"
          >
            <animateTransform
              attributeName="transform"
              type="rotate"
              repeatCount="indefinite"
              dur="1s"
              values="0 50 50;360 50 50"
              keyTimes="0;1"
            ></animateTransform>
          </circle>
        </svg>
      ) : (
        <>
          {columnMap.map((column, i) => (
            <div className="roadmap-column" key={`${column.title}-${i}`}>
              <div className="roadmap-column-heading">{column.title}</div>
              <div className="roadmap-cards">
                {column.tickets.map((t, i) => (
                  <div key={`exploring-${i}`} className="roadmap-cards-item">
                    <div className="roadmap-cards-item-heading">{t.title}</div>
                    <div
                      className="roadmap-cards-item-content"
                      dangerouslySetInnerHTML={{ __html: t.description }}
                    />
                    <svg
                      xmlns="http://www.w3.org/2000/svg"
                      x="0px"
                      y="0px"
                      viewBox="0 0 100 125"
                      style={{
                        enableBackground: "new 0 0 100 100",
                        maxWidth: "18px",
                        cursor: "pointer",
                      }}
                    >
                      <g>
                        <g>
                          <path d="M81.7,40.4H64.6l3.6-17.9c0.5-2.5,0.1-5-1.2-7.1l0,0c-1.5-2.5-4.9-3-7-1.1L29.8,38.7c-1.2,1.1-1.8,2.6-1.8,4.1v33.1    c0,2.4,1.5,4.5,3.7,5.3l12.9,3.4c3.1,1.1,6.4,1.6,9.7,1.6h18c6.6,0,12.5-4.2,14.7-10.5l6.9-20c0.3-0.9,0.4-1.9,0.4-2.9v0    C94.3,46,88.7,40.4,81.7,40.4z" />
                          <path d="M22.2,40.4H7.8c-1.5,0-2.8,1.2-2.8,2.8v34c0,1.5,1.2,2.7,2.7,2.8h14.5c1.5,0,2.8-1.2,2.8-2.8v-34    C25,41.6,23.7,40.4,22.2,40.4z" />
                        </g>
                      </g>
                    </svg>
                  </div>
                ))}
              </div>
            </div>
          ))}
        </>
      )}
    </div>
  );
}

export default Roadmap;
```

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4wgs6v0lxyt7wemci629.gif)
 

---

### 🙌BONUS: Adding upvote to Roadmap tickets

As a bonus, let's add a feature for the users to upvote the tickets. By doing so, we can gain a deeper understanding of what our users are thinking and what they expect from us. Find out more about the benefits of having a public roadmap here. 

**Step 8:** **Mutating Data**

We create a new file to store our mutations in `src/gql/mutations.js`. Taking a reference from auto-generated documentation, we can specify the mutation parameters. 

```jsx
import { gql } from "@apollo/client";

/**
 * gql query to get roadmap
 * The query parameters we got straight from Canonic autogenerated documentation
 */
export const UPVOTE = gql`
  mutation Upvote($ticketId: ID!) {
    createUpvote(input: { ticket: $ticketId }) {
      _id
    }
  }
`;
```

**Step 9: Add upvote logic in the Component**

We can now connect our mutation in `Roadmap` component. In this case, we will save the upvote not only on the backend but also in the local storage since we will not be creating a user on the backend and we want to retain the upvote information.

We make the following changes in `Roadmap.js` and add the mutation logic. Our final file looks like below.

```jsx
import React from "react";
import { useQuery, useMutation } from "@apollo/client";

import { GET_ROADMAP } from "../../gql/queries";
import { UPVOTE } from "../../gql/mutations";

import "./Roadmap.css";

function Roadmap() {
  const { data = {}, loading } = useQuery(GET_ROADMAP);
  const [upvoteTicket] = useMutation(UPVOTE, {
    context: {
      headers: {
        Authorization:
          "617bdcfc530d0d0009c04985-c2ca6caf-485c-4bc1-8ac8-4b9defe2707e",
      },
    },
  });

  const { roadmaps = [] } = data;

  const [upvotes, setUpvotes] = React.useState([]);

  // dividing tickets into their respective categories
  const exploringTickets = roadmaps.filter(
    (t) => t.stage.value === "EXPLORING"
  );
  const inProgressTickets = roadmaps.filter(
    (t) => t.stage.value === "IN_PROGRESS"
  );
  const doneTickets = roadmaps.filter((t) => t.stage.value === "DONE");
  const leavingItForNowTickets = roadmaps.filter(
    (t) => t.stage.value === "LEAVING_IT_FOR_NOW"
  );

  const columnMap = [
    {
      title: "Exploring",
      tickets: exploringTickets,
    },
    {
      title: "In Progress",
      tickets: inProgressTickets,
    },
    {
      title: "Done",
      tickets: doneTickets,
    },
    {
      title: "Leaving it for now",
      tickets: leavingItForNowTickets,
    },
  ];

  const handleUpvoteTicket = React.useCallback(
    async (id) => {
      setUpvotes((upvotes) => [...upvotes, id]);
      localStorage.setItem(`${id}`, true);

      upvoteTicket({ variables: { ticketId: id } });
    },
    [upvoteTicket]
  );

  const isTicketUpvoted = React.useCallback(
    (id) => !!upvotes.find((t) => t === id) || localStorage.getItem(id),
    [upvotes]
  );

  return (
    <div className="roadmap">
      {loading ? (
        <svg
          xmlns="http://www.w3.org/2000/svg"
          style={{
            margin: "auto",
            background: "none",
            display: "block",
            shapeRendering: "auto",
            maxWidth: "30px",
            marginTop: "-20px",
          }}
          width="200px"
          height="200px"
          viewBox="0 0 100 100"
          preserveAspectRatio="xMidYMid"
        >
          <circle
            cx="50"
            cy="50"
            fill="none"
            stroke="#4d5273"
            strokeWidth="10"
            r="35"
            strokeDasharray="164.93361431346415 56.97787143782138"
          >
            <animateTransform
              attributeName="transform"
              type="rotate"
              repeatCount="indefinite"
              dur="1s"
              values="0 50 50;360 50 50"
              keyTimes="0;1"
            ></animateTransform>
          </circle>
        </svg>
      ) : (
        <>
          {columnMap.map((column, i) => (
            <div className="roadmap-column" key={`${column.title}-${i}`}>
              <div className="roadmap-column-heading">{column.title}</div>
              <div className="roadmap-cards">
                {column.tickets.map((t, i) => (
                  <div key={`exploring-${i}`} className="roadmap-cards-item">
                    <div className="roadmap-cards-item-heading">{t.title}</div>
                    <div
                      className="roadmap-cards-item-content"
                      dangerouslySetInnerHTML={{ __html: t.description }}
                    />
                    <svg
                      xmlns="http://www.w3.org/2000/svg"
                      x="0px"
                      y="0px"
                      viewBox="0 0 100 125"
                      style={{
                        enableBackground: "new 0 0 100 100",
                        maxWidth: "18px",
                        cursor: "pointer",
                      }}
                      className={!isTicketUpvoted(t._id) && "not-filled"}
                      onClick={() => handleUpvoteTicket(t._id)}
                    >
                      <g>
                        <g>
                          <path d="M81.7,40.4H64.6l3.6-17.9c0.5-2.5,0.1-5-1.2-7.1l0,0c-1.5-2.5-4.9-3-7-1.1L29.8,38.7c-1.2,1.1-1.8,2.6-1.8,4.1v33.1    c0,2.4,1.5,4.5,3.7,5.3l12.9,3.4c3.1,1.1,6.4,1.6,9.7,1.6h18c6.6,0,12.5-4.2,14.7-10.5l6.9-20c0.3-0.9,0.4-1.9,0.4-2.9v0    C94.3,46,88.7,40.4,81.7,40.4z" />
                          <path d="M22.2,40.4H7.8c-1.5,0-2.8,1.2-2.8,2.8v34c0,1.5,1.2,2.7,2.7,2.8h14.5c1.5,0,2.8-1.2,2.8-2.8v-34    C25,41.6,23.7,40.4,22.2,40.4z" />
                        </g>
                      </g>
                    </svg>
                  </div>
                ))}
              </div>
            </div>
          ))}
        </>
      )}
    </div>
  );
}

export default Roadmap;
```

**Note** be sure to create access_tokens for your canonic APIs for mutating the data. Read more about it [here](https://docs.canonic.dev/concepts/projects/permissions)

### And that's it, you've built the roadmap! 🎉

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rb1jki0pptm5ly5idii8.gif)
 

**Conclusion:** 

Roadmaps for your products offer several advantages, including a better understanding of the strategy and vision, guidance for executing the strategy, facilitation of discussion and opinions, etc.

Follow this step to step guide and build Roadmaps for your own SaaS startups. 

Check out the live demo link [here](https://canonic-roadmap.netlify.app/?utm_source=dev.to&utm_medium=organic_article&utm_campaign=30daysofcanonic_uc_1&utm_term=conclusion_live_demo&utm_content=day_1)

and you can checkout the sample code [here](https://github.com/canonic-dev/canonic-samples#roadmap)

You can also check out our other guides [here](https://dev.to/canonic).

Join us on discord to discuss or share with our community. Write to us for any support requests at [support@canonic.dev](mailto:support@canonic.dev). Check out our [website](https://www.canonic.dev/30-days-of-canonic/?utm_source=dev.to&utm_medium=organic_article&utm_campaign=30daysofcanonic_uc_1&utm_term=conclusion_website&utm_content=day_1) to know more about Canonic. Let us know in the comments below what you think about the guide. Thank you!