Netflix Clone với ReactJS, Styled Components và Firebase (Firestore & Auth) - Phần 2
01 Dec, 2021
TỪ QUỐC HƯNG
AuthorSau phần 1 của series này, chắc hẵn các bạn đã nóng lòng muốn bắt tay ngay vào code rồi đúng không 😁. Okie, trong phần thứ 2 của series này, chúng ta sẽ cùng nhau tạo phần Story của trang Home dành cho khách trên trang Netflix nhé (Trang lúc ta vào mà chưa login ấy 😁). Nào! bắt đâu thôi!

Mục Lục
Sau phần 1 của series này, chắc hẵn các bạn đã nóng lòng muốn bắt tay ngay vào code rồi đúng không 😁. Okie, trong phần thứ 2 của series này, chúng ta sẽ cùng nhau tạo phần Story của trang Home dành cho khách trên trang Netflix nhé (Trang lúc ta vào mà chưa login ấy 😁). Nào! bắt đâu thôi!
I. Tạo file data tĩnh.
Tạo file data tĩnh là sao? Những data tĩnh là những data như hình bên dưới, chúng ta sẽ không lưu trực tiếp trên file .js
hay .jsx
mà chúng ta lưu chúng dưới dạng JSON và load lên thôi.

Trong project của chúng ta, các bạn tạo thư mục fixtures, trong đó ta sẽ tạo 2 file data khác nhau, faqs.json
cho phần FAQs và stories.json
cho các Story của trang, như hình bên dưới.

Trong các file đã tạo ở trên, các bạn thêm data cho nó như sau:
faqs.json
[
{
"id": "dffee64b-bf69-4f7f-a056-e4e99718c2d7",
"header": "What is Netflix?",
"body": "Netflix is a streaming service that offers a wide variety of award-winning TV shows, movies, anime, documentaries, and more on thousands of internet-connected devices.\n\nYou can watch as much as you want, whenever you want without a single commercial – all for one low monthly price. There's always something new to discover and new TV shows and movies are added every week!"
},
{
"id": "308e3980-d9b8-43f6-80a5-2a855bc7364e",
"header": "How much does Netflix cost?",
"body": "Watch Netflix on your smartphone, tablet, Smart TV, laptop, or streaming device, all for one fixed monthly fee. Plans range from 70,000 ₫ to 260,000 ₫ a month. No extra costs, no contracts."
},
{
"id": "a8231110-c2a7-42a3-998a-ec936e8e500d",
"header": "Where can I watch?",
"body": "Watch anywhere, anytime. Sign in with your Netflix account to watch instantly on the web at netflix.com from your personal computer or on any internet-connected device that offers the Netflix app, including smart TVs, smartphones, tablets, streaming media players and game consoles.\n\nYou can also download your favorite shows with the iOS, Android, or Windows 10 app. Use downloads to watch while you're on the go and without an internet connection. Take Netflix with you anywhere."
},
{
"id": "d40d6041-4573-4794-8c60-41afa8aaec3b",
"header": "How do I cancel?",
"body": "Netflix is flexible. There are no pesky contracts and no commitments. You can easily cancel your account online in two clicks. There are no cancellation fees – start or stop your account anytime."
},
{
"id": "e32a5639-6149-474c-8dcc-3231acc18484",
"header": "What can I watch on Netflix?",
"body": "Netflix has an extensive library of feature films, documentaries, TV shows, anime, award-winning Netflix originals, and more. Watch as much as you want, anytime you want."
},
{
"id": "ee556aad-708a-4f6e-8ba9-32921ee885d3",
"header": "Is Netflix good for kids?",
"body": "The Netflix Kids experience is included in your membership to give parents control while kids enjoy family-friendly TV shows and movies in their own space.\n\nKids profiles come with PIN-protected parental controls that let you restrict the maturity rating of content kids can watch and block specific titles you don’t want kids to see."
}
]
faqs.json
stories.json
[
{
"id": "eeba294b-0629-4360-94a4-c085ef78b33d",
"title": "Enjoy on your TV.",
"subTitle": "Watch on Smart TVs, Playstation, Xbox, Chromecast, Apple TV, Blu-ray players, and more.",
"image": "/images/misc/home-tv.png",
"alt": "Tiger King on Netflix",
"direction": "row"
},
{
"id": "071ddd8a-2ba3-4e7d-bf48-b1c1c2c0013a",
"title": "Download your shows to watch offline.",
"subTitle": "Save your favorites easily and always have something to watch.",
"image": "/images/misc/home-mobile.png",
"alt": "Watch on mobile",
"direction": "row-reverse"
},
{
"id": "b5b21681-662a-44b8-83e6-63c7e3cf567a",
"title": "Watch everywhere.",
"subTitle": "Stream unlimited movies and TV shows on your phone, tablet, laptop, and TV.",
"alt": "Money Heist on Netflix",
"direction": "row"
},
{
"id": "d3fdfefb-3d43-4422-84b7-257a72a624b3",
"title": "Create profiles for kids.",
"subTitle": "Send kids on adventures with their favorite characters in a space made just for them—free with your membership.",
"image": "/images/misc/for-kids.png",
"alt": "Netflix for kids",
"direction": "row-reverse"
}
]
stories.json
Note: để có được id như mình thì các bạn lên trang Online GUID / UUID Generator tại đây để có thể lấy các id tự generate ra nhé.
Tiếp theo các bạn tạo thư mục images/misc để chứa hình ảnh cho phần Story Guest Home Page của chúng ta. Hình ảnh các bạn có thể download tại đây.
Bạn cũng có thể đặt tên thư mục tùy ý nhưng phải đảm bảo là sau đó bạn phải edit lại phần data JSON của file stories.json
cho đúng path để ảnh có thể load được nhé 😉.
II. Tạo component Story.
Component Story là sao? Thì component Story là những component nó view những content như hình bên dưới.

Đầu tiên ta cần tạo folder components, trong folder components ta tạo folder và file như sau:

Note:
- story.jsx: file chứa các child component được define để build nên component story.
- index.jsx: nó là một compound component, nó dùng để chứa các component được định nghĩa ở file
story.jsx
và sử dụng nó để build nên component story đồng thời quản lý cácstate
hoặcprops
được truyền vào.
Khái quát một chút về compound component:
Compound components là 1 design pattern trong React, nó là loại component có thể quản lí internal state
của nó và component đó được render như thế nào thì đều nằm ở phía implementation chứ không phải ở declaration .
Okie quay trở lại, trong folder src ta tạo file global-styles.jsx
để css global cho cả trang của chúng ta, ta thêm code vào như bên dưới.
import { createGlobalStyle } from 'styled-components';
export const GlobalStyles = createGlobalStyle`
html, body {
font-family: Arial, Helvetica, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
background-color: #000;
color: #333;
font-size: 16px;
}
`;
global-styles.jsx
Sau đó sử dụng component này ở trong /src/index.js
như bên dưới:
import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
import App from "./App";
import reportWebVitals from "./reportWebVitals";
import { GlobalStyles } from "./global-styles";
ReactDOM.render(
<React.StrictMode>
<GlobalStyles />
<App />
</React.StrictMode>,
document.getElementById("root")
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
index.js
Trong file story.jsx
ta define các child component như sau:
import styled from "styled-components/macro";
export const Inner = styled.div`
display: flex;
align-items: center;
flex-direction: ${({ direction }) => direction};
justify-content: space-between;
max-width: 1100px;
margin: auto;
width: 100%;
@media screen and (max-width: 1000px) {
flex-direction: column;
}
`;
export const BlockPane = styled.div`
width: 50%;
@media screen and (max-width: 1000px){
width: 100%;
padding: 0 45px;
text-align: center;
}
`;
export const Title = styled.h1`
font-size: 50px;
line-height: 1.1;
margin-bottom: 8px;
@media screen and (max-width: 600px){
font-size: 35px;
}
`;
export const SubTitle = styled.h2`
font-size: 26px;
line-height: normal;
font-weight: normal;
@media screen and (max-width: 600px){
font-size: 18px;
}
`;
export const Image = styled.img`
max-width: 100%;
width: 507px;
height: auto;
`;
export const Item = styled.div`
display: flex;
border-bottom: 8px solid #222;
padding: 50px 5%;
color: #fff;
overflow: hidden;
`;
export const Container = styled.div`
@media (max-width: 1000px) {
${Item}:last-of-type h2 {
margin-bottom: 50px;
}
}
`;
story.jsx
Tiếp theo đến với file /story/index.jsx
, vì nó là một compound component, nên tại đây ta có thể quản lý và sử dụng các props được binding vào như sau:
import React from "react";
import { BlockPane, Container, Image, Inner, Item, SubTitle, Title } from "./styles/story";
export default function Story({ children, direction = 'row', ...restProps }) {
return (
<Item {...restProps}>
<Inner direction={direction}>{children}</Inner>
</Item>
);
}
Story.Container = function StoryContainer({ children, ...restProps }) {
return <Container {...restProps}>{children}</Container>
}
Story.BlockPane = function StoryBlockPane({ children, ...restProps }) {
return <BlockPane {...restProps}>{children}</BlockPane>
}
Story.Title = function StoryTitle({ children, ...restProps }) {
return <Title {...restProps}>{children}</Title>
}
Story.SubTitle = function StorySubTitle({ children, ...restProps }) {
return <SubTitle {...restProps}>{children}</SubTitle>
}
Story.Image = function StoryImage({ children, ...restProps }) {
return <Image {...restProps}>{children}</Image>
}
Story.SubTitle = function StorySubTitle({ children, ...restProps }) {
return <SubTitle {...restProps}>{children}</SubTitle>
}
index.jsx
Note:
- children: prop chứa các content của component,
- direction: prop này dùng để nhận các value css của component để sử dụng trong thằng
<Inner />
của filestory.jsx
, - restProps: là một rest parameter.
Okie! define như vậy là đủ rồi hehe 😁, Vậy sử dụng nó như thế nào? Thì để sử dụng những thứ mà chúng ta define nảy giờ như sau:
- Trong file
App.js
import React from 'react';
import Story from './components/story/index';
import storiesData from './fixtures/stories.json'
export default function App() {
return (
<Story.Container>
{storiesData.map(story => (
<Story key={story.id} direction={story.direction}>
<Story.BlockPane>
<Story.Title>{story.title}</Story.Title>
<Story.SubTitle>{story.subTitle}</Story.SubTitle>
</Story.BlockPane>
{story.image ? (
<Story.BlockPane>
<Story.Image src={story.image} alt={story.alt}></Story.Image>
</Story.BlockPane>
) : ''}
</Story>
))}
</Story.Container>
);
}
App.js
Đấy cũng đơn giản phết nhỉ 😁, các component về sau chúng ta cũng tạo theo cách này nhé 😉, run thử xem chạy như thế nào nhé.

Okie! work tốt nhá hehe 😎.
III. Tổng kết.
Okie! Phần thứ 2 của series này tới đây thôi, cơ bản chúng ta cũng làm được 50% Guest Home Page rồi. Đồng thời với ngần này thì đã đủ cho các bạn làm quen và code được theo style của styled-component rồi. Trong phần tiếp theo chúng ta sẽ làm phần FAQs của Guest Home Page này và install thêm các tool như ESLint và Prettier để có thể check convention của chúng ta khi coding giúp code của chúng ta gọn gàng, đẹp và bớt "gà" hơn nhé 😁.