開始 React 專案的五個步驟
許多人在學習 React 時,都是透過網路上的教學,看別人寫程式覺得好像懂了,但真正要從零開始做一個小專案卻會發現不知道該從何下手,或是找不到一個地方起頭。這篇文章就是要教你怎麼去開始一個 React 專案。
以 MDN 提供的 Todo List 小專案為例(範例連結)。
第一步:根據設計圖定義資料結構並且做出假資料
通常我們拿到的設計圖會是一個有資料的畫面,因此我們可以根據畫面來設計假資料。
在這個畫面上,總共有 3 個 task,而且這 3 個 task 是有順序性的,因此首先我們會想到使用陣列來儲存多個 task 的標題:
const tasks = ["Eat", "Sleep", "Repeat"]
但每個 task 除了有標題之外,還有一個 checkbox 來呈現「是否完成」的狀態,因此我們進一步將陣列中的字串改成用物件,來同時儲存標題和狀態:
const tasks = [
{
title: "Eat",
isDone: true
},
{
title: "Sleep",
isDone: false
},
{
title: "Repeat",
isDone: false
}
]
這樣我們的假資料就準備好了!
真實的資料會來自於其他來源(例如 firebase 或是 backend API 等等),不一定會和我們預先準備的假資料結構相同,通常會寫一個負責轉換結構的函式來處理。
第二步:根據設計圖拆解元件
元件是 React 中封裝功能並且可以重複利用的最小單位,拆解的方式會是「一個元件負責單一功能」為原則,以下是個人拆解的方式:
App(藍): 根元件,也就是最外層的元件
— TitleInput(綠上): 負責使用者輸入標題,還有根據標題新增 task 的按鈕
— TaskFilters(綠中): 負責篩選 task 的條件
— Tasks(綠下): 負責顯示篩選出來的 tasks
— — Task(橘): 負責顯示單一個 task
拆解的結果因人或團隊而異。對於一個複雜一點的專案,100 個人去拆解很有可能會有 100 種完全不同的結果,但大致上會符合「單一功能」的原則,實務上也常會根據元件大小(程式碼行數)進一步作拆解,例如寫某個元件只要超過 100 行程式碼就再拆解成更小的元件之類的。
第三步:依照元件拆解,使用假資料產出靜態畫面
首先我們把每個元件的和資料無關的部分刻好,並且依照階層關係組合起來(可先忽略 CSS ):
function Task() {
return (
<div>
<input type="checkbox" />
<button>Edit</button>
<button>Delete</button>
</div>
)
}function Tasks() {
return (
<div>
<h2>3 tasks remaining</h2>
<Task />
<Task />
<Task />
</div>
)
}function TaskFilters() {
return (
<div>
<button>All</button>
<button>Active</button>
<button>Completed</button>
</div>
)
}function TitleInput() {
return (
<div>
<input />
<button>Add</button>
</div>
)
}function App() {
return (
<div>
<h1>What needs to be done?</h1>
<TitleInput />
<TaskFilters />
<Tasks />
</div>
)
}ReactDOM.render(
<App />,
document.getElementById("root")
);
會得到以下結果:
接著將假資料從最外層的 App 元件透過 props 傳進去,並且再進一步往下傳到需要顯示資料的元件中(和資料有關的部分我用粗體標示出來):
function Task(props) {
return (
<div>
<input type="checkbox" checked={props.task.isDone} />
{props.task.title}
<button>Edit</button>
<button>Delete</button>
</div>
)
}function Tasks(props) {
return (
<div>
<h2>{props.tasks.length} tasks remaining</h2>
{props.tasks.map(task => {
return <Task task={task} />
})}
</div>
)
}function TaskFilters() {
return (
<div>
<button>All</button>
<button>Active</button>
<button>Completed</button>
</div>
)
}function TitleInput() {
return (
<div>
<input />
<button>Add</button>
</div>
)
}function App(props) {
return (
<div>
<h1>What needs to be done?</h1>
<TitleInput />
<TaskFilters />
<Tasks tasks={props.tasks} />
</div>
)
}const tasks = [
{
title: "Eat",
isDone: true
},
{
title: "Sleep",
isDone: false
},
{
title: "Repeat",
isDone: false
}
]ReactDOM.render(
<App tasks={tasks} />,
document.getElementById("root")
);
最後得到我們要的靜態畫面:
之所以把假資料從最外層的 App 元件傳進去,而不是從某個內層元件傳進去,是為了在不違反「單向資料流」的原則下,讓每個元件都可以從上層拿到需要呈現的資料。
如果遇到資料不足以呈現畫面的情況,這時候就要回過頭去補假資料甚至要根據補上的假資料來調整資料結構。
第四步:加入呈現畫面所需要的 state
到目前為止我們刻畫好靜態的畫面,但根據使用者互動會呈現出不同的畫面,因此我們需要 state 來決定使用者要看到什麼畫面。
舉例來說,原先 tasks 的資料我們是寫死的,可以將它轉換成 state:
function App(props) {
const [tasks, setTasks] = React.useState(props.tasks)
return (
<div>
<h1>What needs to be done?</h1>
<TitleInput />
<TaskFilters />
<Tasks tasks={tasks} />
</div>
)
}
使用 useState 將 props.tasks 做為初始資料並且轉換成可透過呼叫 setTasks 來修改的 tasks。
第五步:修改 state 以動態呈現畫面
接著我們來實作刪除 task 的功能。
由於刪除的按鈕在 Task 元件中,許多 React 初學者會嘗試在這個元件中去修改傳進來的 props 以達成刪除 task 的目的,但如此一來就違反 props 的唯讀性。
習慣上正確的做法是找到 state 所在的元件中,宣告一個修改 state 的函式,然後再將這個函式透過 props 傳到需要呼叫的元件中。
以刪除 task 為例,tasks 所在的元件為 App,因此我們在 App 元件中宣告一個刪除 task 的函式,然後再傳到 Task 元件中給按鈕呼叫。
function Task(props) {
return (
<div>
<input type="checkbox" checked={props.task.isDone} />
{props.task.title}
<button>Edit</button>
<button onClick={() => {
props.delete()
}}>Delete</button>
</div>
)
}function Tasks(props) {
return (
<div>
<h2>{props.tasks.length} tasks remaining</h2>
{props.tasks.map((task, index) => {
return <Task task={task} delete={delete} index={index} />
})}
</div>
)
}function App(props) {
const [tasks, setTasks] = React.useState(props.tasks)
function delete(indexToDelete) {
const newTasks = tasks.filter((task, index) => {
return index !== indexToDelete
})
setTasks(newTasks)
}
return (
<div>
<h1>What needs to be done?</h1>
<TitleInput />
<TaskFilters />
<Tasks tasks={tasks} delete={delete} />
</div>
)
}
特別注意這個 delete 的函式需要一個 indexToDelete 的參數才知道要刪掉位在哪個 index 的 task,也因此多傳了一個 index 的 props 到 Task 元件中。
結語
在 Todo List 這個小專案中,還有新增、修改、篩選 task 的功能,就留給各位用同樣的概念去練習。
相信在接下來練習的過程中一定還會遇到其他的問題,但希望各位在讀過這篇文章之後,下次來到迷宮之外,至少可以找到迷宮的入口 😇