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.
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.
Summary
This is a rough introduction for my first React app, The most important lesson I learned is “Refactor, refactor and refactor”22.
- React File Structure ↵
- Presentational and Container Components ↵
- Folder Structure in React Apps ↵
- Structuring React Projects — a Definitive Guide ↵
- Fractal — A react app structure for infinite scale ↵
- Structuring projects and naming components in React ↵
- Introducing Hooks ↵
- Making Sense of React Hooks ↵
- Why React Hooks, and how did we even get here? ↵
- Hooks at a Glance ↵
- Hook me up: Intro to React Hooks ↵
- Replacing Redux with the new React context API ↵
- Manage global state with React Hooks ↵
- Building Your Own Hooks ↵
- Simple Code Reuse with React Hooks ↵
- Lazy Loading Routes in React ↵
- How to use React.lazy and Suspense for components lazy loading ↵
- Code-Splitting ↵
- The Cost Of JavaScript In 2018 ↵
- A React And Preact Progressive Web App Performance Case Study: Treebo ↵
- Lessons Learned: Code Splitting with Webpack and React ↵
- The most important lessons I’ve learned after a year of working with React ↵