Create Top 100 Documentaries Site With React Hooks and Material UI

Feb 07, 2019 6 Min read

React Hooks, an awesome feature which is available in React v16.7.0-alpha, is able to simplify React state and lifecycle features from function components. Material-UI, a compact, customizable, and beautiful collection of components for React, is easy to make use of Material Design elements in React web or mobile applications. This post will cover the implementation of React Hooks and Material-UI to build a Top 100 Documentaries Site.

snapshot

Project structure

Project structure is essential for productivity and maintenance. There are tons of structure recommendations out there, such as Brad Frost’s Atomic Design and Redux’s Ducks System. These are best practices I follow1,2,3,4,5,6.

  • Move files around until it feels right
  • Moving files should be effortless
  • Structure should encourage scalability and reusability
  • Separating stateful containers from stateless components.
  • Grouping components by route

    tree
    ├── .env                                Environment variables configuration
    ├── .gitignore                          git ignore configuration
    ├── README.md                           Documentation
    ├── package.json                        npm configuration
    ├── public/                             public resources folder
    └── src                                 Main scripts folder
       ├── components/                    React basic components folder
       ├── data/                          Public data folder
       ├── page                           React routes folder
       │    ├── comment/                  Comment page folder
       │    ├── error/                    Error page folder
       │    ├── home/                     Home page folder
       │    │    ├── containers/          Home page pure components
       │    │    └── index.js             Home page entry file
       │    ├── statistics/               Statistics page folder
       │    └── App.js                    Define root layout and routes
       ├── utils/                         Helper functions folder
       ├── bootstrap.js                   Material-UI stytle initialization
       ├── index.css                      Global style
       ├── index.js                       Main js file
       └── serviceWorker.js               Service worker configuration
    

Data Source

The detail of the documentaries are scraped from IMDB, which is the world’s most popular and authoritative source for movie, TV and celebrity content.

// Execute immediately
(async () => {
  const documentaries = []
  let promises = []

  // Convert documentaries list csv to array
  const CSV = fs.readFileSync(inputFile, 'utf8')
  const ARR = CSVToArray(CSV)
  // Separate array into small chunks to fetch concurrently
  const chunkArray = chunk(ARR, NUM_PER_FETCH)

  for (let arr of chunkArray) {
    arr.forEach(ele => {
      // Enable scrapers
      ele.length > 1 && promises.push(new Scraper(ele[0], ele[1]).fetch())
    })
    await Promise.all(promises).then(res => res.forEach(ele => documentaries.push(ele)))
    // Clear promise array
    promises = []
  }
  // Write result to json file
  fs.writeFileSync(outputFile, JSON.stringify(documentaries))
})()

The final scraped json file:

[
  {
    "docResource": "https://www.imdb.com/title/tt6769208/?ref_=adv_li_i",
    "docTitle": "Blue Planet II",
    "docYear": "2017",
    "imgTitle": "MV5BNjI1M2ZjMzItZWI4Ny00ZWJlLWI0ZDAtMTJhNDQxOWZjM2M5XkEyXkFqcGdeQXVyMjExMjk0ODk@._V1_SY1000_CR0,0,759,1000_AL_.jpg",
    "country": [ "UK" ],
    "summaryText": "David Attenborough returns to the world's oceans in this sequel to the acclaimed documentary filming rare and unusual creatures of the deep, as well as documenting the problems our oceans face.",
    "ratingValue": "9.3",
    "ratingCount": "14,814"
  }
]

React Hooks

React Hooks are presented at React Conf 2018. According to official documentation, Hooks are functions that let you “hook into” React state and lifecycle features from function components7, and as Dan Abramov said, unlike patterns like render props or higher-order components, Hooks don’t introduce unnecessary nesting into your component tree. They also don’t suffer from the drawbacks of mixins8.

There are many benefits of migrating from traditional React features to new React Hooks such as easier state management and cleaner code etc9.

State management

Manage local state inside function components with useState()10

// Traditional class component state management
class App extends Component {
  constructor(props) {
    super(props);
    this.state = { inputValue: '' };
    this.handleInput = this.handleInput.bind(this);
  }

  handleInput(e) { this.setState({ inputValue: e.target.value }) }

  render() {
    return (
      <>
        <input value={this.state.inputValue} onChange={this.handleInput} />
        {this.state.inputValue}
      </>
    )
  }
}
// Functional component state management with Hooks
function App() {
  const [inputValue, setInputValue] = useState("");
  const handleInput = ({ target: { value } }) => setInputValue(value);

  return (
    <>
      <input value={inputValue} onChange={handleInput} />
      {inputValue}
    </>
  );
}

Side effects

Perform component side effects with useEffect()11

// Traditional class component side effects
class App extends Component {
  constructor(props) {
    super(props);
    this.state = { mousePosition: { x: 0, y: 0 } };
    this.handleMousePosition = this.handleMousePosition.bind(this);
  }

  handleMousePosition(e) {
    this.setState({ mousePosition: { x: e.clientX, y: e.clientY } });
  }

  componentDidMount() {
    window.addEventListener("mousemove", this.handleMousePosition);
  }

  componentWillUnmount() {
    window.removeEventListener("mousemove", this.handleMousePosition);
  }

  render() {
    return <div>{`x: ${this.state.mousePosition.x}, y: ${this.state.mousePosition.y}`}</div>;
  }
}
// Functional component side effects with Hooks
function App() {
  const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 });

  useEffect(() => {
    const handleMousePosition = e => setMousePosition({x: e.clientX,y: e.clientY});
    window.addEventListener("mousemove", handleMousePosition);
    return () => {
      window.removeEventListener("mousemove", handleMousePosition);
    };
  }, []);

  return <div>{`x: ${mousePosition.x}, y: ${mousePosition.y}`}</div>;
}

Context consumption

Simplify context consumption with useContext()12,13

// Traditional class component context consumption
function App (){
  return (
    <ThemeContext.Consumer>
      { theme => <Main style={{background: theme.background}}/> }
    </ThemeContext.Consumer>
  )
}
// Functional component context consumption with Hooks
function App (){
  const theme = useTheme();
  return <Main style={{background: theme.background}}/>
}

Stateful logic reuse

Share stateful logic across multiple components with custom hooks14,15

// Custom Hooks
const useTitle = title => useEffect(() => { document.title = title }, [title]);

const useHover = (initial = false) => {
  const [hover, setHover] = useState(initial)
  const onMouseEnter = useCallback(() => setHover(true), [])
  const onMouseLeave = useCallback(() => setHover(false), [])
  return [hover, {onMouseEnter, onMouseLeave}]
}

Lazy loading

The React.lazy function enables dynamic import components and routes (code splitting)16,17,18, which is critical for better performance.

Components

const Child = React.lazy(() => import('./components'));

const Main = () => (
  <Suspense fallback={<div>Loading...</div>}>
    <Child />
  </Suspense>
)

Routes

import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
import React, { Suspense, lazy } from 'react';

const Home = lazy(() => import('./page/Home'));
const About = lazy(() => import('./page/About'));

const App = () => (
  <Router>
    <Suspense fallback={<div>Loading...</div>}>
      <Switch>
        <Route exact path="/" component={Home}/>
        <Route path="/about" component={About}/>
      </Switch>
    </Suspense>
  </Router>
);

Material-UI

Material-UI is currently the most popular React UI framework, it provides a lot of awesome features to help developers build beautiful UI with little efforts, such as CSS in JS and custom hooks etc.

const useStyles = makeStyles(theme => ({
  progress: {
    margin: theme.spacing.unit * 4,
    color: theme.color
  }
}));

function App() {
  const theme = useTheme();
  const classes = useStyles();
  return (
    <CircularProgress
      className={classes.progress}
      size={theme.spacing.unit * 10}
    />
  );
}

ReactDOM.render(
  <ThemeProvider theme={createMuiTheme({ color: "#212121" })}>
    <App />
  </ThemeProvider>,
  document.querySelector("#root")
);

Helper functions

This is a tiny project without database. All of the data manipulations are in client side with helper functions.

// Sort documentaries by keys, such as sort by year
const sortBy = (arr, compare) =>
  arr.map((item, index) => ({item, index}))
    .sort((a, b) => compare(a.item, b.item) || a.index - b.index)
    .map(({item}) => item)

// Count documentaries for statistics, such as count by year
const countBy = (arr, fn) =>
  (fn ? arr.map(typeof fn === 'function' ? fn : val => val[fn]) : arr).reduce((acc, val) => {
    acc[val] = (acc[val] || 0) + 1
    return acc
  }, {})

Performance

Interactive sites often send too much JavaScript to clients, which is bad for use experiences because JavaScript is is a kind of expensive resource and it can delay user interactivity19.

There are many effective ways to optimize react apps such as Server Side Render, Code Splitting, Lazy Loading and Inline Critical Resources20,21.

audits

Summary

This is a rough introduction for my first React app, The most important lesson I learned is “Refactor, refactor and refactor”22.