Practicing Tech Design with React, Storybook, and Nivo

I spent the weekend making a technical design and implementing it to challenge myself and evaluate a few different unfamiliar libraries. I've read a lot of positive reviews of Storybook, so I figured creating a small project would be a good way to evaluate. Driven by my interest in Nivo, a charting library, I thought an easy bit of data to visualize would be my TreeHouse profile points.

The React Suspense API and react-fetching-library were technologies that have piqued my interest lately, so I included these intentionally. I’m rather happy with the result other than having to create my own node server to get around a Treehouse server issue (or rather an issue with the client library. More on that later).

I first created a technical design to plan out the broad, architectural pieces needed for the app. The process involved researching what would be needed from the newer and unfamiliar technologies and formulating a procedure for implementing them.

Then, I executed my plan and took note of my opinions regarding the unfamiliar technologies and the hiccups along the way.

The result

Self-Chart Technical Design

High Level Design

Chosen Technologies

High level Todo items

  • Bootstrap a development cycle by creating a storybook version of components needed in this project.
  • Create UI layer with React.
  • Define tests, components, client, and data transformation.
  • Create client for fetching needed data.
  • Create data transformation utility for UI consumption.

No state management library is needed for this simple example. Though no other library is really needed either for something this simple. I just want to learn about them.

Low Level Design

Setup

  1. Use create-react-app with typescript to get up and running.
  2. Start a git repo.
  3. Deploy on codesandbox to show the world.

Storybook

Dependencies:

  yarn add
  yarn add -D typescript
  yarn add -D awesome-typescript-loader
  yarn add -D @types/storybook__react
  1. Create a .storybook directory and add config.js file
  2. Create storybook script in your build system

    • In our case, this is npm.

      • In package.json write script "storybook": "start-storybook -p 6006 -c .storybook
  3. Configure the storybook config file with the following script:

    import { configure } from "@storybook/react"
    
    // This will iterate through src
    // looking for any extension that matches stories.ts
    
    const req = require.context("../src", true, /stories.ts$/)
    
    function loadStories() {
      req.keys().forEach(file => req(file))
    }
    
    configure(loadStories, module)
  4. For Storybook to work with TypeScript, create a custom webpack config inside at

    .storybook/webpack.config.js

    module.exports = ({ config }) => {
      config.module.rules.push({
        test: /\.(ts|tsx)$/,
        use: [
          {
            loader: require.resolve("awesome-typescript-loader"),
          },
        ],
      })
      config.resolve.extensions.push(".ts", ".tsx")
      return config
    }
  5. Create tsconfig

.storybook/tsconfig.json

{
  "compilerOptions": {
    "outDir": "build/lib",
    "module": "commonjs",
    "target": "es5",
    "lib": ["es5", "es6", "es7", "es2017", "dom"],
    "sourceMap": true,
    "allowJs": false,
    "jsx": "react",
    "moduleResolution": "node",
    "rootDirs": ["src", "stories"],
    "baseUrl": "src",
    "forceConsistentCasingInFileNames": true,
    "noImplicitReturns": true,
    "noImplicitThis": true,
    "noImplicitAny": true,
    "strictNullChecks": true,
    "suppressImplicitAnyIndexErrors": true,
    "noUnusedLocals": true,
    "declaration": true,
    "allowSyntheticDefaultImports": true,
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "build", "scripts"]
}

PieChart Component

  1. Create a chart component under src/components named PieChart.ts

     import * as React from "react"
    
     const PieChart = () => {
     	return ...
     }
  2. Import the pie chart component from @nive/pie

    import { ResponsivePie } from "@nivo/pie"
  3. Use the component within the PieChart component we created
  4. Create a chart component under src/components named PieChart.tsx

     import * as React from "react"
     import { ResponsivePie } from '@nivo/pie'
    
     const PieChart = () => {
     	return (
     		<ResponsivePie
     			...
     		/>
     	)
     }
  5. Define data to be used in pie chart

     import * as React from "react"
     import { ResponsivePie } from '@nivo/pie'
    
     /*
     	Required props are: { data }
    
     	Parent element is required to have width and height
     	when using responsive chart components
    
     	data type is Array<{
            id:    string | number,
            value: number
     	}>
     */
    
     const PieChart = (data) => {
     	return (
     		<ResponsivePie
     			...
     			data={data}
     		/>
     	)
     }

Storybook Component Setup

  1. Create an pie_chart.stories.tsx file next to the PieChart.tsx component file

    import * as React from "react"
    
    import { storiesOf } from "@storybook/react"
    import { PieChart } from "./PieChart"
    
    /*
     	Stories are to demonstrate the capabilities of a component
     	In this instance, we will need to supply data to the pie chart
     */
    
    storiesOf("PieChart", module).add(
      "Demonstrate simple data passed to chart",
      () => {
        ;<PieChart data={{ exampleData }} />
      },
    )

To run storybook : npm run storybook

Client

Dependencies:

yarn add react-fetching-library
  1. Create the directory src/client
  2. Create the file api.ts

    and setup to use library:

    import { createClient } from "react-fetching-library"
    
    // Often the host endpoint would be hidden in evironment variables
    const HOST = "HOST ENDPOINT HERE"
    
    export const requestHostInterceptor = host => client => async action => {
      return {
        ...action,
        endpoint: `${host}${action.endpoint}`,
      }
    }
    
    export const Client = createClient({
      requestInterceptors: [requestHostInterceptor(HOST)],
    })
  3. Create the query action in

src/client/actions/fetchData.ts

export const fetchData = {
  method: "GET",
  endpoint: "/",
}
  1. Create the client provider component and suspense wrapper

    src/index.ts

     	...
     import { ClientContextProvider } from 'react-fetching-library';
     import { Client } from './api/Client';
    
     const App = () => {
       return (
         <ClientContextProvider client={Client}>
    		<Suspense fallback={<ProgressSpinner />}>
    			{// Whatever children need the client>}
    		</Suspense>
         </ClientContextProvider>
       );
     };
     ...
  2. Create container component for chart component to handle the query for data

    src/components/chart_container.tsx

     ...
     import { useQuery } from 'react-fetching-library';
     import { fetchData } from '../client/actions'
    
     export const ChartContainer = () => {
     	const { loading, payload, error, query } = useQuery(fetchData)
    
     	if (error) return <ErrorButton onClick={query}/>
     	// Will add some data transformation step here
    
     	return <PieChart data={transformedData} />
     }

Data transformation Utility

  • The incoming data from Treehouse is not the correct shape and we need to transform it into a usable shape for the UI
  1. Create the directory src/util
  2. Inside that directory, create the file fetchData_util.ts
  3. Create a function in the file that takes json data of this shape:

     {
     	"name": "Beavis"
     	"points": {
     		"total": 12345,
     		"javascript": 2345,
     		"ruby": 0,
     		...
     	}
     }

    and produces data of this shape:

     [
       {
         "id": "javascript",
         "label": "javascript",
         "value": 2345,
       },
     	...
     ]

    Removing the "total" and any data with the value of 0

  4. Use this utility function within the component that fetches the data

    import { useQuery } from "react-fetching-library"
    import { fetchData } from "../client/actions"
    
    export const ChartContainer = () => {
      const { loading, payload, error, query } = useQuery(fetchData)
    
      if (error) return <ErrorButton onClick={query} />
    
      const transformedData = transformData(payload)
    
      return <PieChart data={transformedData} />
    }
  5. Create the directory tests
  6. Create the file tests/fetchData_test.js
  7. Write some tests considering all possibilities for the utility function

    describe("Confirm transformData", () => {
      test("Should remove key of total from remote data")
      test("Should remove objects with value of 0")
    })

Spinner

  1. Create the spinner component under the components directory
  2. Should be a simple emoji animated to spin with css animations
  3. Use as the suspense fallback

Retrospective

There were a few surprises when implementing this. Some were in Storybook while getting it set up. After following the documentation rather than a tutorial, I got it to work properly.

The other more frustrating surprise was that the client library I used makes an OPTIONS request with every GET request and the Treehouse server was not playing along. I would receive a 500 error on the OPTIONS request, then never get to a successful GET request. By using Postman, I could confirm a GET would work fine without the OPTIONS request. The client library didn't have a way to configure the fetch to drop the OPTIONS request so I ended up creating a node server to be a proxy to the data. The node server made the same GET request, then passed it along to my app. 3 hours later, problem solved.

Conclusions

StoryBook

Storybook seems like an awesome tool to help guide the design of components. I see a cost in learning how to use it and the time spent in designing a component. One could easily burn hours designing a single component. I enjoy Storybook's workflow of isolating a component and its influence on making decisions of a components API. I look forward to learning more about this tool.

React-fetching-library

This library is newer (2 months old as of writing this!) but I have really good feelings about it already. The docs are well written and very helpful. The composability is great from my perspective. After they work out a few kinks, it could be a great tool for componentized fetch requests.

Nivo

I chose Nivo for charting mostly because their site is pretty. The documentation is really great as well. This being the first time using it, I thought it was rather easy to get started and figure out how to plug it into my project. A lot of the styling of charts is handled for you, and some may consider that a plus or a minus, but most of it is customizable. It's no D3 ;) but being the react nerd that I am, I found it to be quite an easy and visually pleasing way to jumpstart charts in my project. I look forward to using this library in the future.

Tech designs

As mentioned, part of this weekend activity was to practice technical designs. Our principal engineer wrote a post on the importance of technical design that goes more in-depth on its value. After using this approach for my weekend hacking extravaganza, I saw how it definitely kept my head out of the details of the code and focused on the bigger picture. After getting together a "template" to follow, implementing the code went rather quickly, then much of the time was spent debugging smaller problems.

This project was admittedly an overuse of libraries and tools, but I now have a functioning template to create charts using React, Nivo, and Storybook.

TL;DR

  • Storybook seems great for large projects that have many reused components.
  • Nivo makes very pretty charts. Great documentation. Understandable API.
  • React-fetching-library has great composability. It is young but has great potential.
  • Creating a technical design greatly increases the speed of development and reveals the difficult parts sooner.

Repos can be found here:

Treehouse Chart

Treehouse Profile Server

«
Developer Wellness
Prototyping vs. Production Development: How to Avoid Creating a Monster
»