Skip to main content

Avada Coding Standard

Giới thiệu tổng quan

Avada Development team là team phát triển sản phẩm cho Avada dựa trên các công nghệ mới như NodeJS, React, Firebase Google Cloud. Vì các công nghệ còn rất mới nên việc tiếp cận với các chuẩn về coding thời gian đầu gặp nhiều trở ngại và thiếu tính nhất quán. Do đó, trong documentation này, chúng ta sẽ cùng thống nhất lại các coding standard mà team Avada nhằm mục đích giúp quá trình viết code được đồng nhất về mặt cú pháp, thiết kế, tính sử dụng lại và hiệu quả tối ưu nhất.

Các phần chính

Trong document này, chúng ta sẽ chia ra làm 5 phần chính gồm:

  • Thống nhất coding standard cho JS và Nodejs.
  • Thống nhất coding standard cho ReactJS
  • Thống nhất coding standard và hiệu năng khi sử dụng Firebase Cloud.
  • Thống nhất một số quy tắc clean code chính
  • 1 số vấn đề về key và bảo mật

Code style standard cơ bản (JS)

Sử dụng chuẩn của Eslint và Prettier

Trong bất kì project nào của Avada đều đã có cài sẵn prettiereslint trong list dependency, trong quá trình code m.n lưu ý sử dụng prettier để format lại code, bằng gõ npm run eslint-fix để chạy toàn bộ project.

Ngoài ra nếu sử dụng Webstorm, có thể sử dụng nút chạy eslint ngay trong IDE. Nút này cũng có thể sử dụng với cả thư mục.

Optimize Imports

Sau nhiều lần thêm thắt chỉnh sửa code có thể dẫn đến việc các phần import bị thừa, cần phải được xóa đi để code được clean, không bị thừa thãi. Tuy nhiên việc làm thủ công có thể dẫn đến việc mất thời gian, không cần thiết. Ở đây khuyên nên sử dụng tính năng Optimize imports của Webstorm để tiết kiệm thời gian.

Để sử dụng, chúng ta chọn vào thư mục src của project, sau đó chọn Optimize imports. Lưu ý chỉ nên optimize import ở thư mục source code, tránh optimize import chạy ở cả project, bao gồm node_modules sẽ dẫn đến mất thời gian.

Đặt tên hàm và biến.

Sử dụng camelCase cho function, variable và properties.

Trong việc khai báo function, variable hay properties phải sử dụng camelCase, không được viết thường hay viết hoa không đồng nhất, dẫn đến khó hiểu cho người đọc. Đối với việc đặt tên hàm chỉ có ngoại lệ với ReactJS component. Điều này ta sẽ đề cập sau.

Sai:

Đúng:

Sử dụng UpperCamelCase cho class

Tuy ở trong NodeJS, chúng ta sử dụng Functional Programming (FP), nhưng cũng ko ít trường hợp ta vẫn phải khai báo class. Và đương nhiên là vẫn sẽ phải theo chuẩn. Với class. Chúng ta phải sử dụng UpperCamelCase và ngược lại không dùng UpperCamelCase với variable, hay function, sẽ gây ra sự khó hiểu cho người đọc code.

Sử dụng UPPERCASE cho hằng số (constant)

Nếu có một giá trị không đổi trong code, cần phải refactor biến này thành constant, việc đặt tên cho constant phải theo chuẩn sử dụng UPPERCASE.

Đặt tên hàm và biến có ý nghĩa, tránh việc đặt tên khó hiểu

Việc đặt tên có vẻ sẽ là việc gây bí cho khá nhiều dev vì nhiều khi vốn từ thiếu, khó tìm cách đặt tên sao cho dễ hiểu. Tuy nhiên, đây là một standard cần tuân theo nên khi code, mọi người cần chịu khó đặt tên cho đúng ngữ cảnh, rèn luyện dần dần. Một số quy tắc cơ bản trong việc đặt tên biến như sau:

  • Tên của function phải bắt đầu bằng động từ. Thí dụ: hàm phải tên là doSomething, chứ ko phải something, hay somethingDo.
  • Tên của boolean phải bắt đầu bằng is, has. Ví dụ như: const isActive = true, const hasPaid = false.
  • Tên của variable khác nên là danh từ.

Sử dụng const thay vì let

Trong functional programming nói chung hay NodeJS nói riêng, việc hạn chế gán lại giá trị cho 1 biến là cần thiết, nó giúp lập trình viên sẽ chỉ có thể khai báo 1 biến với 1 tên 1 lần mỗi block.

Ví dụ, việc sử dụng const như cách dưới và hạn chế việc code thiếu tường minh và buộc lập trình viên phải đặt tên biến có ý nghĩa hơn. Đây cũng là ý avoid mutations trong functional programming.

Import ở đầu file, không import ở trong thân hàm

Rule này áp dụng với bất kì ngôn ngữ lập trình nào, toàn bộ việc import các module khác từ thư viện hay trong source code đều phải ở phía trên cùng của file, không được viết trong thân hàm hay phía giữa file, dẫn đến khó tìm, performance leakage.

Sử dụng toán tử === thay vì ==

Toán tử === sẽ check cả việc so sánh hai giá trị có cùng loại dữ liệu chứ không chỉ dừng lại ở giá trị không thôi. Ví dụ sau:

Sử dụng async/await thay vì Promise hay callback

Trong quá trình code, việc NodeJS từ bản 8 đã hỗ trợ async/await, còn ở phía browse đã có Babel transpile code, nên việc sử dụng async await là cần thiết, giúp code bất đồng bộ dễ đọc hơn.

Tìm hiểu sự tiến hóa trong cách tiếp cập bất đồng bộ tại bài guide này.

Sử dụng arrow function: () =>

Từ bản cập nhật ES6, arrow function đã trở thành 1 tính năng mới phổ biến trong JS và NodeJS. Việc sử dụng arrow function giúp code gọn hơn. Nhìn vào ví dụ phía dưới để thấy thu gọn code sau khi 3 lần sử dụng arrow function.

Ngoài ra, khi return giá trị trực tiếp, arrow function còn có cú pháp viết tắt như sau. Cách viết này sẽ được thấy nhiều ở trong React.

Hạn chế nhiều parameter cho 1 functions

Nếu một hàm có nhiều hơn 3 params phải chuyển qua input param kiểu object và destruct. Với cách viết này, việc truyền vào sẽ ko phải quan tâm tới thứ tự truyền vào param như cách 1 giúp cho việc sử dụng các hàm có nhiều param trở nên dễ sử dụng hơn. Quy tắc này áp chung cho JS và NodeJS, và cũng như React.

Đặt param mặc định cho function khi là boolean

Khi giá trị mặc định là boolean, chúng ta phải luôn ưu tiên đặt giá trị mặc định là false. Như ví dụ dưới đây, nếu trong trường hợp NoSQL database, field hasPaid trong document user không tồn tại dẫn đến việc truyền vào param hasPaid có giá tị undefined. Với undefined truyền vào giá trị mặc định sẽ được sử dụng, nên do đó hàm dưới nghiễm nghiên trả về kết quả user có thể sử dụng dịch vụ mà chưa trả tiền.

Hạn chế side effect, sử dụng pure function

Đây là 1 quy tắc cho NodeJS và cả JS, hay cả Functional Programming nói chung. Ý tưởng của Pure Function ở đây là mọi tác động của một function sẽ chỉ ảnh hưởng đến những biến diễn ra trong function, nó ko thay đổi giá trị của biến nằm ngoài function scope của nó.

Nhìn vào ví dụ trên, cách 1 sẽ sửa trực tiếp vào biến param cart truyền vào, mà JS truyền theo reference, nên cơ bản cart param chỉ là địa chỉ của biến cart trên bộ nhớ. Do đó, biến cart sẽ bị thay đổi. Tưởng tượng trong source code không chỉ có 1, mà 10 chỗ cùng tác động vào biến cart như thế, như vậy, khi xảy ra lỗi, cần truy xem giá trị biến cart thay đổi ntn sẽ rất khó khăn.

Như vậy, cách làm là mỗi hàm chỉ có thể trả về giá trị mới, sau đó giá trị đó sẽ được assign vào một biến khác. Với cách này, mỗi biến có thể có tên riêng thể hiện trạng thái cụ thể.

Một điều có thể nhiều người chọn cart sửa thẳng giá trị của biến cart nghĩ đó sẽ là tại sao cứ phải khai báo biến mới cho tốn bộ nhớ, dùng 1 biến thôi cho tiết kiệm. Thì sẽ có 1 số lý do như sau:

  • Mỗi biến khai báo trong NodeJS, JS đều có block scope nếu khai báo bằng const hoặc let. Nên khi ra khỏi block, hàm đều sẽ được xóa đi. Bộ nhớ sẽ thêm xóa hàm nhiều hơn. Nhưng đổi lại ko có hàm global. Và việc này đã nằm trong điểm mạnh của JS.
  • Việc khai báo nhiều biến rồi cũng sẽ được xóa cũng ko làm RAM nặng thêm nhiều vì đã được clean liên tục. Nhưng bù lại sẽ có được sự tường minh rõ ràng cho từng dòng code.

Folder structure cho Firebase functions (NodeJS)

Việc code NodeJS của Avada không 100% dựa trên framework có sẵn nên việc chia thư mục và có design pattern cho code cũng cần được thống nhất. Sau đâu sẽ liệt kê một số loại thư mục cho backend Avada hay NodeJS nói chung để m.n cùng follow.

Nhìn vào cấu trúc thư mục trên, ta phải để ý một số thư mục quan trọng như sau

  • Config
  • Const
  • Helpers
  • Presenters
  • Handlers
  • Services

Thư mục config

Thư mục này chưa các giá trị biến môi trường theo Firebase Cloud functions. Các biến đi theo functions thì cần phải được khai báo chung trong thư mục này.

Thư mục const

Const ở đây là constant (hằng số). Mỗi giá trị là số bất định, không nay đổi, nhưng lập đi lập lại trong code nhiều thì phải được viết vào thư mục const để dễ quản lí. Nhưng const có liên quan tới nhau phải đưa vào cùng 1 file:

Những giá trị hằng số này, sau khi viết vào definition file cũng cần được viết vào phần type để người khác khi sử dụng lại, có thể viết được tập giá trị mà field có thể nhận:

[!NOTE]

Nếu tách const ra để riêng rồi, không thêm các functions cu nodeJS hay các functions phải import thư viện vào trong file, chỉ dùng để lưu const. Tránh TH nếu hàm bên scripttag cần dùng tới biến, phải import cả 1 thử viện của NodeJS, Firebase hoặc những hàm không cần thiết trong module.

Thư mục handlers

Giống như trong mô hình MVC, các endpoint API, những hàm Firebase Function Handler đều phải viết vào thư mục handlers. Nếu một handler là 1 API với nhiều endpoint và chức năng  khác nhau có thể tạo thêm nhiều controller bên trong để dễ bề quản lý:

Cũng giống như mô hình MVC. Handler hay controller chỉ đóng voi trò thể hiện logic, không xử lý logic phức tạp bên trong. Những tác vụ liên quan tới database, API bên thứ 3 phải được xử lý qua repository hay services, như ví dụ sau:

Có thể thấy bên trên có những hàm tương tác với Shopify đều phải đi qua helpers, service và repository.

Thư mục repository

1 repo chỉ nên kết nối với 1 collection

Thư mục repository giống với vai trò của model trong mô hình MVC. Tuy nhiên có điểm khác biệt là model trong mô hình MVC có định nghĩa về schema cho dữ liệu, còn với firebase là NoSQL không có cấu trúc dữ liệu sẵn.

Quy tắc ở đây là tất cả logic tương tác CRUD với 1 collection trong firebase sẽ phải viết ở trong 1 repository, và ngược lại, logic CRUD với 1 collection chỉ viết trong 1 repository. Hạn chế viết logic của 2 collection trong cùng 1 repository:

Nên đặt repo collection ref là collection, hoặc thống nhất đặt tên giữa các collection

Khi viết như thế này, nếu khi các repository có các hàm gần giống nhau có thể copy dễ dàng được mà không cần phải sửa lại từ chargesRef sang customersRef chẳng hạn.

Thư mục services

Chính vì repository được chưa logic tương tác với 2 collection trở lên, như vậy handler sẽ phải dùng nhiều repository 1 lúc, như việc bartender sử dụng nhiều chai rượu để mix với nhau.

Tuy nhiên, các repository còn có thể được gộp thành các service. Tiêu chí để gom các repository thành service là dựa trên tính năng. Các repository có liên quan với nhau về một tính năng cần được gộp vào 1 service. Tưởng tượng như bartender có một bộ các chai rượu để mix các dòng đồ uống khác nhau, đắt tiền, rẻ tiền.

Thí dụ như các repository tương tác với collection người dùng và collection phân quyền cần được gom vào authService.

Bên cạnh đó, việc giao tiếp với API bên thứ 3 cũng phải được cho vào 1 file trong thư mục service riêng.

Thư mục helpers

Đây là một thư mục khá đặc biệt được gặp trong khá nhiều framework khác nhau. Helpers sẽ là những hàm riêng lẻ, chỉ phục vụ một mục đích nhất định và khó phân loại theo tính năng.

Như ảnh dưới, helper createScriptTags sẽ sử dụng ShopifyApiService để tương tác với bên thứ 3 là Shopify API. Các tác vụ với API phục vụ khác nhau được gom vào 1 logic gọi chung là createScriptTags để sau này có thể sử dụng lại.

Thư mục presenter

Nhiều khi trong handler hay controller chúng ta gặp phải những đoạn phải chọn lại các field trong một array các object để loại đi các field không cần thiết. Những tác vụ này nếu viết vào handler sẽ trông rất dài mà nhiều khi người khác đọc cũng không có nhu cầu hiểu 1 đoạn map, forEach dài dòng phục vụ mục đích format dữ liệu, không ảnh hưởng quá nhiều business logic làm gì. Do đó, phải đưa các tác vụ này vào thư mục presenters:

Optimization cho NodeJS

Sử dụng bất đồng bộ của NodeJS, JS

Tổng quan lại thì NodeJS hay JS là ngôn ngữ lập trình chạy đơn luồng, non-blocking và bất đồng bộ.

NodeJS sẽ chạy trên 1 luồng tức cứ hiểu như là chỉ có 1 người làm việc. Non-blocking là người này sẽ làm việc không tuần tự, việc B không phải đợi việc A làm xong mới được làm, bất đồng bộ ở đây là 1 người có thể làm nhiều việc 1 lúc.

So sánh với các ngôn ngữ đa luồng thì tương đương có nhiều hơn 1 người làm việc. Tuy nhiều đa luồng sẽ dùng cho các các vụ nặng. Còn NodeJS là 1 người đa nhiệm, làm được nhiều việc 1 lúc, cơ mà nhiều việc không quá nặng 1 lúc. Điển hình như các tác vụ I/O, gọi API, thích hợp cho mô hình Cloud, Microservice.

Đối với các kiến trúc cũ, server và database có thể trên cùng 1 server, dẫn đến việc query database ảnh hưởng CPU, mất thời gian. Tuy nhiên với mô hình microservice của NodeJS, nó tương tác với database như tương tác API 1 bên thứ ba qua HTTP. Nên cơ bản nó không làm gì ngoài đợi, nếu chỉ có việc đợi thì tốt nhất làm nhiều việc 1 lúc. Như chủ shop sẽ đưa đơn ship cho bên vận chuyển cùng 1 lúc, chứ không đợi từng đơn gửi xong mới gửi tiếp.

Để sử dụng bất đồng bộ của NodeJS ta sử dụng hàm Promise.all. Nhìn ví dụ dưới ta có thể thấy việc sử dụng bất đồng bộ của NodeJS cho ta tiết kiệm được thời gian khá tốt. Tưởng tượng với khối lượng API request lớn thì sẽ tiết kiệm được rất nhiều thời gian và resource.

Ví dụ

Đây là 1 case thật trong quá trình review code, code ban đầu trong như sau:

/**
*
* @param ctx
* @returns {Promise<{data: unknown[], success: boolean}|{success: boolean}>}
*/
export async function getThirdPartyScripts(ctx) {
try {
const shop = await authentication.getShop(ctx);
const shopify = initShopify(shop);
const pageUrls = await Promise.all(
THIRD_PARTY_SCANNING_PAGES.map(async type => ({
type,
url: await getUrlByType({type, shop})
}))
);
await updateScriptManagerSnippet({shop, shopify});
const thirdPartyScripts = {};
await Promise.all(
pageUrls
.filter(({url}) => url)
.map(async ({type, url}) => {
const {scriptTags} = await getFullPageContent(url, shop.id);
scriptTags?.forEach(scriptTag => {
if (isUndefined(thirdPartyScripts[scriptTag])) {
thirdPartyScripts[scriptTag] = {
url: scriptTag,
id: scriptTag
};
}
thirdPartyScripts[scriptTag][type] = true;
});
})
);
const {scriptManager} = await getSettings(shop.id, 'scriptManager');
const newThirdPartyScripts = await Promise.all(
Object.values(thirdPartyScripts).map(async newScript => {
const existedOldScript = scriptManager?.thirdPartyScripts?.find(
script => script.url === newScript.url
);
if (existedOldScript) {
return existedOldScript;
}
newScript.size = await getResourceSizeFromUrl({url: newScript.url});
return newScript;
})
);
return (ctx.body = {
success: true,
data: Object.values(newThirdPartyScripts)
});
} catch (e) {
console.error(e);
return (ctx.body = {
success: false
});
}
}

Đoạn code trên có nhiều đoạn không sắp xếp hợp lí việc chạy bất đồng bộ để tối ưu, cộng với việc hàm viết thiếu tường minh, cần điều chỉnh lại để tốt hơn:

/**
*
* @param ctx
* @returns {Promise<{data: unknown[], success: boolean}|{success: boolean}>}
*/
export async function getThirdPartyScripts(ctx) {
try {
const shop = await authentication.getShop(ctx);
const shopify = initShopify(shop);
const [onPage3rdPartyScripts, {scriptManager}] = await Promise.all([
detectScriptsFromPages({shop}),
getSettings(shop.id, 'scriptManager'),
updateScriptManagerSnippet({shop, shopify})
]);
const inSetting3rdPartyScripts = scriptManager?.thirdPartyScripts || [];
const newThirdPartyScripts = await Promise.all(
Object.values(onPage3rdPartyScripts).map(script => {
return getScriptWithSize({savedScripts: inSetting3rdPartyScripts, script});
})
);

return (ctx.body = {
success: true,
data: Object.values(newThirdPartyScripts)
});
} catch (e) {
console.error(e);
return (ctx.body = {
success: false
});
}
}

/**
*
* @param {Shop} shop
* @returns {Promise<{}>}
*/
async function detectScriptsFromPages({shop}) {
const pageUrls = await Promise.all(
THIRD_PARTY_SCANNING_PAGES.map(async type => ({
type,
url: await getUrlByType({type, shop})
}))
);

const thirdPartyScripts = {};
const validUrls = pageUrls.filter(({url}) => url);
await Promise.all(
validUrls.map(async ({type, url}) => {
const {scriptTags} = await getFullPageContent(url, shop.id);
scriptTags?.forEach(scriptTag => {
if (isUndefined(thirdPartyScripts[scriptTag])) {
thirdPartyScripts[scriptTag] = {
url: scriptTag,
id: scriptTag
};
}
thirdPartyScripts[scriptTag][type] = true;
});
})
);

return thirdPartyScripts;
}

/**
*
* @param savedScripts
* @param script
* @returns {Promise<*|(*&{size: (Promise<number>|*)})>}
*/
async function getScriptWithSize({savedScripts, script}) {
const hasExistedOldScript = savedScripts?.find(savedScript => savedScript.url === script.url);
if (hasExistedOldScript) {
return hasExistedOldScript;
}
const newScriptSize = await getResourceSizeFromUrl({url: script.url});

return {...script, size: newScriptSize};
}

Quản lý package

dependency hay dev-dependency

Trong NodeJS và JS có NPM là Package Manager, mọi dependency trong source code đều được định nghĩa trong file package.json. Dependency chia ra làm dependency để chạy code và dev-dependency chỉ cần thiết trong quá trình dev.

Việc để ý cài các package vào dependency hay dev-dependency cần được chú ý. Nếu không còn dùng tới thì nên xóa đi.

Version của các package

Có code là sẽ có lỗi, không bao giờ có code mà không lỗi cả. Code mà không lỗi thì chắc chắn có vấn đề.

Điều này cũng đúng với mọi package, việc package update lỗi gây ra lỗi trên cả hệ thống là điều hoàn toàn có thể xảy ra. Nên do đó recommend một số điều sau:

  • Cài package có uy tín. Nếu package không ổn định chỉ để install 1 version cụ thể:
  • Không tự ý lên chấm đầu của 1 package như ko tự ý lên v1.7.0 lên v2.0.0. Việc update mà ko test lại sẽ gây ra lỗi nhiều. Một package khi update chấm ở đầu tức có thay đổi nhiều về API.
  • Không cài các package public bản alpha beta trừ package nội bộ của Avada.

Coding Standard cho ReactJS

Giới thiệu chung

Phần này sẽ không bàn về hướng dẫn viết ReactJS cho người mới bắt đầu, mà sẽ chỉ thống nhất lại quy tắc viết ReactJS cho người đã và đang viết ReactJS cho phù hợp với tiêu chuẩn của team Avada Coding Best Practice

Tên component và tên file component phải giống nhau, và đều viết UpperCamelCase

Thí dụ component tên Example phải ở trong file Example.js. 1 file chỉ nên viết 1 component mà thôi

Các thư mục folder trong app React phải là camelCase, chỉ thư mục chứa component với viết UpperCamelCase.

Nên cấu trúc file CSS/SCSS vào trong thư mục của component, để tránh CSS/SCSS của component ảnh hưởng các component khác

Như ở ảnh dưới thì file css của component CampaignGrid sẽ được đặt luôn trong thư mục chứa component này. Giúp việc tìm css liên quan sẽ dễ dàng hơn. Sau này, khi component được sử dụng lại sang project khác, hay đưa vào thư viện cũng. Như ở ảnh dưới thì file dễ dàng triển khai hơn.

Nếu có một số CSS/SCSS global cho app, to sẽ cho tất cả CSS/SCSS vào 1 thư mục styles.

CSS nên sử dụng cách đặt tên BEM

Việc đặt lên biến đã là một việc khá mất thời gian đôi khi gặp khó khăn, thì việc đặt tên class CSS cũng không thua kém gì. Do đó, việc đặt tên class CSS cũng phải theo chuẩn không chỉ ở trong React mà ở mọi nơi dùng CSS.

Ở đây, chúng ta sẽ thống nhất sử dụng quy tắc BEM để đặt tên cho các class CSS. Chi tiết về cách đặt tên BEM có thể tìm hiểu ở bài viết sau nên xin phép sẽ ko đề cập lại.

M.n có thể nhìn ví sau đầy để hiểu cách sử dụng BEM và SCSS trong project làm sao để CSS chỉ tác động đến 1 component duy nhất mà nó tác động. Tránh xuất hiện những class không có prefix, ảnh hưởng đến CSS của toàn trang.

Phải để ý sử dụng lazy load, codespliting, tree shaking

Việc để sử sử dụng React.lazy là rất cần thiết trong React, kể cả ở React chạy ở phía admin app lẫn ở phía storefront scripttag. Những trường hợp cần để ý buộc dùng React lazy load như sau:

Code spliting

Thí dụ component là dạng Modal, ít trong 1 số điều kiện mới xuất hiện, có condition trong JSX. Thí dụ như modal hiện ra reviews có load thêm thư viện về Firebase Storage để upload ảnh. Nếu cứ mỗi lần load trang đều load ra thì sẽ rất nặng. Chỉ khi mở modal, tức lúc người dùng muốn để lại reviews thì mới lazy load ra.

Dynamic import

Dùng Dynamic Import dạng import(). Nhiều khi bạn cần dùng 1 thư viện như cryptoJS để lấy hash thông tin sau khi người dùng vào 1 link có param mã hóa dạng ?hashCode=YKJKLFJDbla.... Đoạn này hash để lưu thông tin người dùng, để làm tracking conversion link. Tuy nhiên, không phải lúc nào link tracking cũng được người dùng click vào mà SDK của bạn chạy dưới mọi page load. Khi đó, bạn cần import() thư viện trên chỉ khi bạn xác định URL có param thôi. Như vậy sẽ giảm thiểu việc load thư viện.

  useEffect(() => {
(async () => {
const hash = new URL(window.location.href).searchParams.get('hash');
if (!hash) return;
const {decodeCrypto} = await import('../../helpers/cryptoHelpers');
const data = decodeCrypto(hash, KEY_REFERRAL);
setReferralData(data);
})();

setTimeout(() => setShowReferralPopup(true), 5000);
}, []);

Không dùng toán từ && để lọc phần tử trong array

Như đoạn dưới, nếu dùng toán tử && để lọc phần tử trong array nó thành false lúc vào React nó sẽ render lỗi vì false ko có title và action. Ít nhất cũng phải filter Boolean array dưới.

      <Page
...
secondaryActions={[
{
content: 'Export reviews',
onAction() {
openExportModal(true);
}
},
isCreateReview(shop) && {
content: 'Import reviews',
onAction() {
history.push('/reviews/import');
}
}
].filter(Boolean)}
>

Tree shaking

Để ý khi thư viện bạn dùng có tree shaking hay là không. Thí dụ như đoạn code sau

// nên làm
import AES from 'crypto-js/aes';


AES.decrypt(hash.replace(/ /g, '+'), KEY);

// không nên làm

import crypto from 'crypto-js';

crypto.AES.decrypt(hash.replace(/ /g, '+'), KEY);


crypto-js còn rất nhiều module con nữa. Nếu làm theo kiểu 2, bạn đang load toàn bộ thư viện crypto-js vào, sẽ làm nặng codebase lên rất nhiều vì trong crypto-js còn rất nhiều module con nữa.

Không sử dụng inline style tràn lan, không dùng sẵn component của Polaris

Thí dụ như đoạn code trên là bạn đang sử dụng không đúng quy tắc:

  • Sử dụng tràn lan inline style
  • Không tách component nhỏ ra
  • Không sử dụng component Polaris sẵn có

const {
openModal: openReviewOptimizeModal,
modal: reviewOptimizeModal,
closeModal
} = useConfirmModal({
confirmAction: () => {
if (unsavedChange) {
saveAction();
}
return optimizeAction(isOptimizing);
},
title: 'Are you sure to apply these settings?',
content: (
<div>
<div className={'Avada-Box'}>
<Stack vertical>
<Stack distribution="leading">
<Icon source={CircleAlertMajor} color="highlight" />
<Heading>Optimization effects</Heading>
</Stack>
<div style={{marginLeft: '35px'}}>
{[
{
title: 'Image Optimizationss:',
content:
// eslint-disable-next-line camelcase
compressType === 'auto'
? `Optimize Image Automatically for ${
// eslint-disable-next-line camelcase
statusProduct === QUERY_ALL_PRODUCT
? 'all product images'
: 'only published product images'
}`
: `Custom image quality for ${
// eslint-disable-next-line camelcase
statusProduct === QUERY_ALL_PRODUCT
? 'all product images'
: 'only published product images'
// eslint-disable-next-line camelcase
} (${qualityPct}%)`,
enabled: enabled
},

{
title: 'Alt Optimization:',
// eslint-disable-next-line camelcase
content: `Set up keyword-rich alt text for ${productOvr ? 'Product' : ''} ${
// eslint-disable-next-line camelcase
collectionOvr ? ',Collection' : ''
// eslint-disable-next-line camelcase
} ${articleOvr ? ',Blog post' : ''} ${fileOvr ? ',File' : ''} images`,
enabled: altEnabled
},
{
title: 'Image Filename:',
content:
'Auto convert your image filename for Product, Collection, Blog post images',
enabled: filenameEnabled
}
].map(item => {
if (item.enabled) {
return (
<Stack key={item.title} spacing="extraTight">
<TextStyle variation="strong">{item.title}</TextStyle>
<TextStyle>{item.content}</TextStyle>
</Stack>
);
}
})}
</div>
<div style={{flexBasis: '30%', justifyContent: 'space-around', display: 'flex'}}>
<div className="Avada-Box__ReviewProduct">
<div className="Avada-Box__ImageProduct">
{isOptimizePreviewImage && (
<div>
<Spinner />
</div>
)}
{!isOptimizePreviewImage && (
<img
style={{width: '100%', height: '100%', borderRadius: '4px'}}
src={previewImage?.urlOrginal}
/>
)}
</div>
<div className="Avada-Box__DetailProduct">
{isOptimizePreviewImage && (
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
padding: '30px'
}}
>
<Spinner />
</div>
)}
{!isOptimizePreviewImage && (
<div>
<div
style={{
width: '100%',
display: 'flex',
justifyContent: 'space-between',
marginBottom: '10px'
}}
>
<h1
style={{
font: 'Poppins',
fontSize: '16px',
fontWeight: 'bold',
lineHeight: '24px',
letterSpacing: '0px'
}}
>
ORIGINAL
</h1>
<img src={beforeIcon} />
</div>
<div className="Avada-Box__TextDetail">
File size
{previewImage['image'] ? (
<span style={{color: '#D92117'}}>
{' '}
{(previewImage['image'].oldSize / 1024).toFixed(2)} KB
</span>
) : (
' --'
)}
</div>
<div className="Avada-Box__TextDetail">
Alt text before:{' '}
{previewImage['alt']?.oldImage
? truncateString(previewImage['alt'].oldImage, 12)
: '--'}
</div>
<div className="Avada-Box__TextDetail">
File name before:{' '}
{previewImage['filename']?.oldImage
? truncateString(previewImage['filename'].oldImage, 12)
: '--'}
</div>
</div>
)}
</div>
</div>
<div style={{marginTop: '150px'}}>
<img src={vsIcon} />
</div>
<div className="Avada-Box__ReviewProduct">
<div className="Avada-Box__ImageProduct">
{isOptimizePreviewImage && <Spinner />}
{!isOptimizePreviewImage && (
<img style={{width: '100%', height: '100%'}} src={previewImage?.urlOrginal} />
)}
</div>
<div className="Avada-Box__DetailProduct">
{isOptimizePreviewImage && (
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
padding: '30px'
}}
>
<Spinner />
</div>
)}
{!isOptimizePreviewImage && (
<div>
<div
style={{
width: '100%',
display: 'flex',
justifyContent: 'space-between',
marginBottom: '10px'
}}
>
<h1
style={{
font: 'Poppins',
fontSize: '16px',
fontWeight: 'bold',
lineHeight: '24px',
letterSpacing: '0px'
}}
>
OPTIMIZED
</h1>
<img src={afterIcon} />
</div>
<div className="Avada-Box__TextDetail">
File size
{previewImage['image'] ? (
<span style={{color: '#4B9F28'}}>
{' ' + (previewImage['image'].newSize / 1024).toFixed(2)} KB
</span>
) : (
' --'
)}
</div>
<div className="Avada-Box__TextDetail">
Alt text after:{' '}
{previewImage['alt']?.newImage
? truncateString(previewImage['alt'].newImage, 12)
: '--'}
</div>
<div className="Avada-Box__TextDetail">
File name after:{' '}
{previewImage['filename']?.newImage
? truncateString(previewImage['filename'].newImage, 12)
: '--'}
</div>
</div>
)}
</div>
</div>
</div>
</Stack>
</div>
<TextStyle>You can change the Optimization Settings </TextStyle>
<Link
removeUnderline
onClick={() => {
closeModal();
setIsShowPopup(true);
}}
>
here
</Link>
</div>
),
buttonTitle: 'Apply',
successCallback: () => {}
});

Có thể viết lại đúng như sau:


const {
openModal: openReviewOptimizeModal,
modal: reviewOptimizeModal,
closeModal
} = useConfirmModal({
confirmAction: () => {
if (unsavedChange) {
saveAction();
}
return optimizeAction(isOptimizing);
},
title: 'Are you sure to apply these settings?',
content: (
<ModalConfirmApplySetting
settings={settings}
previewImage={previewImage}
isOptimizePreviewImage={isOptimizePreviewImage}
/>
),
buttonTitle: 'Apply',
successCallback: () => {}
});

/**
*
* @param settings
* @param isOptimizePreviewImage
* @param previewImage
* @return {Element}
* @constructor
*/
const ModalConfirmApplySetting = ({settings, isOptimizePreviewImage, previewImage}) => {
const {
enabled,
alt_enabled: altEnabled,
filename_enabled: filenameEnabled,
compress_type: compressType,
article_ovr: articleOvr,
product_ovr: productOvr,
collection_ovr: collectionOvr,
file_ovr: fileOvr,
quality_pct: qualityPct,
status_product: statusProduct
} = settings;

const effects = [
{
title: 'Image Optimization:',
content:
// eslint-disable-next-line camelcase
compressType === 'auto'
? `Optimize Image Automatically for ${
// eslint-disable-next-line camelcase
statusProduct === QUERY_ALL_PRODUCT
? 'all product images'
: 'only published product images'
}`
: `Custom image quality for ${
// eslint-disable-next-line camelcase
statusProduct === QUERY_ALL_PRODUCT
? 'all product images'
: 'only published product images'
// eslint-disable-next-line camelcase
} (${qualityPct}%)`,
enabled: enabled
},
{
title: 'Alt Optimization:',
// eslint-disable-next-line camelcase
content: `Set up keyword-rich alt text for ${productOvr ? 'Product' : ''} ${
// eslint-disable-next-line camelcase
collectionOvr ? ',Collection' : ''
// eslint-disable-next-line camelcase
} ${articleOvr ? ',Blog post' : ''} ${fileOvr ? ',File' : ''} images`,
enabled: altEnabled
},
{
title: 'Image Filename:',
content: 'Auto convert your image filename for Product, Collection, Blog post images',
enabled: filenameEnabled
}
];

return (
<>
<Stack vertical>
<Banner status={'info'} title={'Optimization effects'}>
{effects.map(item => {
if (item.enabled) {
return (
<Stack key={item.title} spacing="extraTight">
<TextStyle variation="strong">{item.title}</TextStyle>
<TextStyle>{item.content}</TextStyle>
</Stack>
);
}
})}
</Banner>
<Stack distribution={'fillEvenly'} alignment={'center'}>
<ProductBox
isLoading={isOptimizePreviewImage}
previewImage={previewImage}
title={'ORIGINAL'}
icon={beforeIcon}
sizeField={'oldSize'}
type={'oldImage'}
sizeColor={'#D92117'}
/>
<Stack distribution={'center'} alignment={'center'}>
<img src={vsIcon} alt={'vs icon'} />
</Stack>
<ProductBox
isLoading={isOptimizePreviewImage}
previewImage={previewImage}
title={'OPTIMIZED'}
icon={afterIcon}
sizeField={'oldSize'}
type={'newSize'}
sizeColor={'#4B9F28'}
/>
</Stack>
<TextStyle>
You can change the Optimization Settings{' '}
<Link
removeUnderline
onClick={() => {
// closeModal();
setIsShowPopup(true);
}}
>
here
</Link>
</TextStyle>
</Stack>
</>
);
};

ModalConfirmApplySetting.propTypes = {
settings: PropTypes.object,
isOptimizePreviewImage: PropTypes.bool,
previewImage: PropTypes.string
};

/**
*
* @param isLoading
* @param previewImage
* @param type
* @param sizeField
* @param title
* @param icon
* @param sizeColor
* @return {Element}
* @constructor
*/
const ProductBox = ({
isLoading,
previewImage,
type = 'oldImage',
sizeField = 'oldSize',
title = 'ORIGINAL',
icon = beforeIcon,
sizeColor = '#D92117'
}) => {
return (
<div className="Avada-Box__ReviewProduct">
<div className="Avada-Box__ImageProduct">
{isLoading && <Spinner />}
{!isLoading && (
<img
style={{width: '100%', height: '100%', borderRadius: '4px'}}
src={previewImage?.urlOrginal}
alt={'preview image'}
/>
)}
</div>
<div className="Avada-Box__DetailProduct">
{isLoading && (
<Stack distribution={'center'} alignment={'center'}>
<Spinner />
</Stack>
)}
{!isLoading && (
<div>
<Stack distribution={'fillEvenly'}>
<Heading>{title}</Heading>
<img src={icon} alt={'icon'} />
</Stack>
<div className="Avada-Box__TextDetail">
File size
{previewImage['image'] ? (
<span style={{color: sizeColor}}>
{' '}
{(previewImage['image'][sizeField] / 1024).toFixed(2)} KB
</span>
) : (
' --'
)}
</div>
<div className="Avada-Box__TextDetail">
Alt text before:{' '}
{previewImage['alt']?.[type] ? truncateString(previewImage['alt'][type], 12) : '--'}
</div>
<div className="Avada-Box__TextDetail">
File name before:{' '}
{previewImage['filename']?.[type]
? truncateString(previewImage['filename'][type], 12)
: '--'}
</div>
</div>
)}
</div>
</div>
);
};

ProductBox.propTypes = {
isLoading: PropTypes.bool,
previewImage: PropTypes.object,
type: PropTypes.oneOf(['oldImage', 'newImage']),
sizeField: PropTypes.oneOf(['oldSize', 'newSize']),
title: PropTypes.string,
icon: PropTypes.any,
sizeColor: PropTypes.string
};




Standard viết code React có tính reusable cao

Sử dụng Functional Component

Không nên sử dụng Class Component, chính React đã khuyến cáo không nên sử dụng nữa. Nếu viết Class Component sẽ cho dev cảm giác sử dụng OOP, viết ra component cha trước rồi mới extend ra component con. Việc này trái với lối tư duy Functional Programming của React.

Với React, thà dùng lại nhiều thành phần nhỏ nhưng không bị dùng thừa hơn là chỉ dùng 1 component nhỏ cơ mà lại phải sử dụng cả các tính năng ko cần thiết của component cha, hay còn gọi là prefer composition over inheritance

API phải thân thiện

API ở đây không hiểu là là REST HTTP API, mà API hiểu chung là Application Programming Interface tức là cách tương tác với component.

Ví dụ component phải được thiết kế như sau:

Như thấy ở bên trên, ta sẽ thấy có những đặc điểm sau:

  • Có khả năng tùy biến với những prop option cho phép người sử dụng component tùy chỉnh animationSpeed.

  • Có state value(open) và handle(onClose) dễ hiểu trong ngữ cảnh. Sheet component là một dạng modal hiện ở trên trang. Nên khi sử dụng, người dùng sẽ chỉ cần hiểu là truyền vào trạng thái khi nào mở, hoặc khi đóng thì sẽ làm gì.Việc khai báo state sẽ là ở phía người sử dụng component.

  • Có prop children, dễ cho việc hiện thị content JSX bên trong component.

Còn nếu như sau sẽ là cách làm không nên làm:

Component sẽ có những nhược điểm sau:

  • Thiết kế này là extract code rồi bỏ vào 1 file chứ ko phải refactor. Refactor code là vừa đưa code ra 1 file khác gọn hơn, vừa dễ dàng tái sử dụng. Nếu ntn, khi sử dụng, người sử dụng sẽ phải truyền 1 cặp state vào component và ko hiểu component này sẽ sử dụng set state (setOpen) thành giá trị ntn. Và người lại, ở phía component cũng sẽ ko định nghĩa được người dùng component nên truyền vào dạng giá trị nào. Ví dụ tránh thiết kế component như thế này: Component trên bắt ta phải truyền cả cặp state vào mà ko rõ rằng nó được sử dụng trong trường hợp ntn.

  • Không sử dụng children, khiến việc thêm child content trở nên không tường minh. Nhìn hình dưới sẽ thấy cách triển khai 1 thể hiện việc content innerContent nằm bên trong Sheet rõ ràng hơn.

Về việc thiết kế một component tốt nên tìm hiểu cách Polaris xây dựng một hệ thống React UI cho Shopify. Nhìn vào một component được thiết kế tốt như sau. Mọi props đều có tên rất dễ hiểu, cấu trúc dữ liệu input của các prop cũng đều rất tốt, tường minh.

Sử dụng React Context

Một trong những vấn đề đau đầu của React là việc truyền nhiều prop xuống từ component cha xuống sâu tới component con tới 2-3 lớp. Như vậy sẽ gặp tình trạng truyền prop cho những component không dùng gì tới prop đó ngoài việc truyền tiếp xuống component phía dưới.

Hoặc nhiều khi là muốn truyền theo chiều ngang nhưng React không cho phép, lúc đó ta sẽ phải sử dụng một component cha, đưa state vào context để giúp cho các component đều có thể sử dụng state mà không phải trực tiếp nhận prop truyền vào.

Chi tiết cách sử dụng React Context ở đây.

Sử dụng React Hooks

React Hooks là một trong những tính năng cập nhật 2019 được cộng đồng đánh giá cao nhất, nó giúp việc refactor code React trở nên vô cùng dễ dàng. React bản chất cũng giống như một Functional Component, đều là 1 function.

Vậy để khi refactor một đoạn logic với function thì sẽ làm gì? Đơn giản là bỏ logic và trong thân hàm, thêm param phía ngoài. React Hooks chính là vậy.

Hãy nhìn ví dụ hook useConfirmModal sau để thấy sức mạnh của React Hook:

Việc sử dụng Hook này cũng rất flexible, dễ dàng đáp ứng nhiều use case khác nhau:

Một ví dụ điển hình nữa của React Hook là hook useFetchApi giúp tiết kiệm công sức gọi khai báo state mỗi lần gọi API. Ảnh hơi dài nên m.n xem ảnh ở link sau

Sử dụng setState đúng cách

Với React có 2 cách để setState đó là nhận thẳng state mới hoặc truyền vào 1 function callback trả ra state mới setState(prev => {return prev}). Với cách 2 này, thì prev luôn là trạng thái state mới nhất, và việc lấy state nhanh hơn, ko nên dùng biến state hiện tại để merge, mutate và gán vào chính hàm setState.

Standard khi sử dụng Firebase, Google Cloud.

Firestore không có hàm COUNT, SUM

Một điểm trừ của Firestore hay các database NoSQL nói chung đó là nó không có hàm count, hàm sum. Nếu google sẽ thấy một số bài viết dạng bài này tức họ khuyển là nên đọc tất cả các document cần tìm ra thành dạng array rồi sau đó tính length của array đó.

Cách làm này không nên được áp dụng trong trường hợp không có bất kì giải pháp nào khác. Thông thường để đếm với NoSQL chúng ta sẽ thêm 1 field thí dụ: commentCount vào trong mỗi document post. Như vậy mỗi lần thêm mới sẽ cộng dồn vào document post.

note

Update 2023, with @google-cloud/firestore v6, now it support count, sum, average. You can update and skip this standard.

Check docs empty

Để check xem các docs đọc ra từ database có trả về rỗng hay không, ta thường check theo cách docs.docs.length > 0 hay docs.size > 0, thực ra Firestore có sẵn prop empty rồi, chỉ cần viết !docs.empty là được.

Không cần dùng FieldValue.serverTimestamp()

Khi truyền vào các field createdAt, updatedAt ta thường muốn lấy timestamp thời điểm hiện tại, tuy nhiên để làm thế, ta chỉ cần truyền new Date() vào là sẽ được. Giờ sẽ luôn là giờ ISO, không phải import FieldValue làm gì cả.

Không lấy nhiều thông tin hơn cần thiết

Đối với Firestore mỗi document lấy ra sẽ đều tính là 1 read, tức là 1 lần truy vấn, đều tính tiền cho chúng ta. Nếu chỉ cần lấy 1 document mà đọc ra 1k document xong lấy mỗi phần tử đầu tiên của array sẽ là thừa, ảnh hưởng đến billing.

Hoặc như nếu như chỉ cần lấy tất cả contacts để gửi mail mà có email, thì phải filter từ đầu vào where để lấy ra ít document nhất có thể, tránh việc lấy ra hết contacts ko hợp lệ xong tiếp tục dùng hàm filter của JS.

Bên cạnh đó, nếu document nặng, lấy ra nhiều dữ liệu sẽ gây mất thời gian, cần select ra những field thực sự cần thiết, bỏ đi nhưng field nặng để quá trình truy vấn diễn ra nhanh hơn.

Sử dụng Google Cloud Pub/Sub cho những tác vụ cần scale lớn, hoặc chạy ngầm.

Pub/Sub là viết tắt của Publish/Subscribe tức một hệ thống messaging giao tiếp giữa các microservice giúp việc thực thi các tác vụ bất đồng bộ dễ dàng hơn.

Thí dụ như có tác vụ sau, người dùng upload ảnh avatar lên, yêu cầu là phải tối ưu hóa ảnh của người dùng sao cho nhẹ nhất. Để làm tác vụ này, ta phải tránh làm việc tối ưu hóa ảnh ngay trong quá trình request HTTP lưu ảnh và trả về API response.

Thay vào đó, chúng là trả về thành công ngay sau khi thông tin được update vào database. Sau đó publish một message cho một Google Cloud Function xử lý tác vụ tối ưu ảnh ngầm. Như vậy người dùng sẽ không phải đợi lâu khi tác vụ tối ưu ảnh mất thời gian.

Syntax thực hiện Pubsub như sau: Publish ra message. Tạo 1 function lắng nghe sự kiện. Viết logic xử lý sự kiện trong function.

Các quy tắc clean code

Early return, hạn chế tối đa dùng else

Tưởng tượng có hàm sau, hàm này dùng khá nhiều else, cộng với vi phạm standard chúng ta đã đề ra phần trước về hạn chế sửa lại biến trong JS.

Hàm sau có thể viết gọn lại như thế này. Code trông sẽ gọn, logic thoáng hơn. Cho ta cảm giác khi đọc đến return rồi thì hết 1 logic, không phải tưởng tưởng if/else trong đầu.

Hàm nên chỉ làm 1 việc

Cũng như việc trong 1 công ty thường có 1 số người làm 1 việc và 1 việc duy nhất nhưng rất tốt. Function cũng vậy, nó chỉ nên làm 1 việc và làm đúng, đủ. Đó là quy tắc single responsibility.

Một hàm chỉ nên có 1 lớp đoạn code abstraction

Đoạn code abstraction tức là những đoạn code như check regex email, for each một list user, thay đổi data của mỗi users. Nếu mỗi code block như thế ko có tên riêng mà viết trong cùng 1 hàm sẽ gây ra khó đọc. Ví dụ như sau:

Với đoạn code trên có 2 logic riêng biệt nhưng lại bị mix vào nhau dẫn đến khó hiểu, không reuse được. Nên được viết như sau. Bên cạnh đó, hàm tokenize và parse nên được tách ra module riêng.

Nếu điều kiện của if quá phức tạp, viết vào thành function

Nếu từ 2 điều kiện của if trở lên, nên đưa câu check điều kiện vào thành 1 function với tên dễ hiểu để đảm bảo code dễ đọc hơn. Ví dụ như dưới:

Clean code là code không cần comment

Ngày trước m.n có thể nghe rằng lập trình viên không tốt sẽ code mà không comment. Điều này không đúng. Chỉ comment cho những những đoạn code có mức phức tạp cao. Còn lại, việc comment là không cần thiết, việc comment chỉ cho thấy là code của bạn khó hiểu, nên phải giải thích.

Ngược lại, code clean sẽ không cần comment vì code đã tường minh dễ hiểu sẵn rồi, không tội gì mà phải comment làm gì cho mất thời gian.

Ví dụ đoạn code như thế này là comment thừa:

Thay vì viết comment, hãy viết docblock.

Viết docblock và definition file cho code

JS là ngôn ngữ dynamic type, tức nó ko quá chặt chẽ như những ngôn ngữ typing như Java, như phiên bản khác của JS là Typescript. Do đó vừa để được thoải mái, vừa có typing cho code, phải sử dụng doc block kết hợp với definition file.

Nhìn ví dụ bên trên khi dev khác code có thể viết object item có những property gì bởi database NoSQL không có cấu trúc có định, nên xem cấu trúc database sẽ không nắm hết được.

Bên cạnh typing, docblock còn nhằm để documentation code, đưa description, link reference hay note thêm nhiều thông tin khác nữa. Nhìn vào ví dụ phía dưới ta sẽ thấy viết comment sẽ trông chuyên nghiệp hơn so với việc viết comment từng dòng một.

Một số lưu ý về bảo mật

Không commit serviceAccount key, credentials vào source code.

Service account key thường có quyền vào database, deploy, có thể ảnh hưởng đến project rất lớn. Credential API key của bất kì service nào cũng vậy, không viết vào trong code để tránh trường hợp bị lộ source code sẽ không ảnh hưởng tới data.

Do đó phải luôn để ý file .gitignore xem có những file nào đang được commit, whitelist tên file chưa credentials.

note

This is also related to the QA/QC checklist You should sync with this checklist as well since it is not coding standard only.

Nếu dùng Big Query, hạn chế dùng dưới dạng query cho truyền param từ API.

Việc sử dụng Bigquery sẽ dễ bị ảnh hưởng bởi SQL injection, mọi câu query nếu mang tính report thì có thể hạn chế cho người dùng query thì càng tốt. Còn không, phải đảm bảo strip trước khi đưa vào Big Query.

Không cài những package không đảm bảo

Khi cài bất kì package nào nên cẩn thận xem số sao trên github, số lượng cài trên NPM để cân nhắc trước khi cài. Tránh dùng những package kém chất lượng dẫn đến mất bảo mật.

Một số lỗi thường gặp Khi sử dụng Firebase query

Khi get collection để lấy collection.docs, không cần check docs size hay empty rồi mới đọc collection.docs.map

Nếu cần trả về array docs, không dùng map và push từng document vào array. Return map là tạo 1 array rồi.

Shopify Polaris coding standard

Button: có thể dùng props url như Link. Tránh viết onClick window.open. Dùng url thì render ra thẻ , onClick thì render