React開發入門:以開發Todo List為例

概述

起因是,為了做畢設,順便學點前端,我打算學習React。

MDN通過一個Todo List App的製作,教導React的知識點。

這是我在MDN學習React的總結,總結出了一些React開發的基本特性,加上MDN上這個教程的簡體中文還沒翻譯過來,我的總結就更有意義了。

我的成品放在了Github

React基本概念

JSX是什麼?

JSX語法,是長得很像HTML的JavaScript程式碼,簡稱:JavaScript語法的類HTML擴展(後面會介紹,它和HTML的區別)

const header = (
  <header>
    <h1>Mozilla Developer Network</h1>
  </header>
);

關於它的一些原理:瀏覽器是無法理解JSX的,JSX語法會在編譯的時候,被轉換:

const header = React.createElement("header", null,
  React.createElement("h1", null, "Mozilla Developer Network")
);

設置React APP

初始化APP

npx create-react-app moz-todo-react

應用結構

moz-todo-react
├── README.md
├── node_modules
├── package.json
├── package-lock.json
├── .gitignore
├── public
│   ├── favicon.ico
│   ├── index.html
│   └── manifest.json
└── src
    ├── App.css
    ├── App.js
    ├── App.test.js
    ├── index.css
    ├── index.js
    ├── logo.svg
    └── serviceWorker.js
  • src是源碼
  • public包含了在開發APP過程中,瀏覽器需要讀取的文件;index.html是最重要的文件,React會將src中的源碼注入這個文件,是的瀏覽器能夠運行源碼;其中有個<title>標籤,是應用的便簽上顯示的應用名

探索第一個React組件<App />

在React中,component是一個代表APP某部分的可重用的模組。

App.js 由三個主要部分組成:頂部的一些 import 語句、中間的App組件和底部的 export 語句。大多數React組件都遵循這種模式。

// 第一個語句導入React庫本身。因為React將我們寫入的JSX轉換為React.createElement(),所以所有的React組件都必須導入React模組。如果跳過這一步,應用程式將產生一個錯誤。
import React from 'react';
// 注意./被使用在路徑的開頭,.svg擴展名在路徑的結尾——這告訴我們文件是本地的,而且這不是JavaScript文件。
import logo from './logo.svg';
// 第三條語句導入了與我們的App組件相關的CSS。
import './App.css';

// 這個App函數返回一個JSX表達式。這個表達式定義了瀏覽器渲染到DOM的內容。
function App() {
  return (
    // 表達式中的某些元素具有屬性,這些屬性的編寫方式與HTML類似,遵循attribute="value"的模式。
    // <div>標籤有一個className屬性。這與HTML中的class屬性相同,但因為JSX是JavaScript,所以我們不能使用class這個詞——它是保留的,這意味著JavaScript已經將它用於特定目的,這將在我們的程式碼中引起問題。
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <p>
          Hello, World!
        </p>
      </header>
    </div>
  );
}

// 在App.js文件的最底部,export default App語句使我們的App組件可以被其他模組使用。
export default App;

index.js

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';

ReactDOM.render(<App />, document.getElementById('root'));

調用React的ReactDOM.render()函數,有2個參數:

  1. 被渲染的組件,在這個例子中是<App />。
  2. 被渲染的DOM元素,在本例中是ID為root的元素。如果你查看public/index.html內部。你會看到這是<body>中的<div>元素。

變數和props

JSX中的變數

注意App.js文件中的這一行:

<img src={logo} className="App-logo" alt="logo" />

這裡,<img />標籤的src屬性值用花括弧括起來。

JSX就是這樣識別變數的。React會識別{logo},知道你是在應用程式的第2行導入logo,然後檢索logo文件並渲染它。

使用自定義變數,定義say Hello的對象subject

function App() {
  const subject = "React";
  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <p>
          Hello, {subject}!
        </p>
      </header>
    </div>
  );
}

組件props

  • prop是傳遞到React組件中的任何數據。React props與HTML屬性類似。HTML元素有屬性,React組件有props。
  • props是在組件調用時直接定義和初始化的,使用與HTML屬性相同的語法——prop=”value”。(你可以將使用一個組件,看作是調用一個組件的函數,函數的返回值是JSX表達式)
  • 在React中,數據流是單向的:props只能從父組件向下傳遞到子組件;props是只讀的。
props使用實例

打開index.js,在調用<App />時,直接定義和初始化props。

ReactDOM.render(<App subject="Clarice" />, document.getElementById('root'));

在App.js中,接收props。

function App(props) {
  const subject = props.subject;
  return (
    // return statement
  );
}

小結(React基本概念)

  • 組件可以import需要的module,且必須在文件底部export自己。
  • 組件函數使用PascalCase命名。(可以理解為,將camelCase的第一個單詞首字母大寫)
  • 您可以通過將變數放在花括弧之間,在JSX中讀取它們,比如{so}。
  • 為了不與JavaScript保留字衝突,一些JSX屬性與HTML屬性不同。例如,HTML中的class轉換為JSX中的className。注意,多單詞屬性是camelCase的。
  • Props就像HTML屬性一樣,在組件調用時被初始化,並被傳遞到組件中。

常用特性(以Todo List App的開發為例)

迭代(遍歷)渲染

需求:Todo List會由很多待辦事項,即Todo item組成。通過迭代渲染,我們可以很優雅的渲染一系列的Todo component。

通常用來渲染一個JSX對象數組。

初始化<Todo />數組:

const taskList = props.tasks.map(task => (
  <Todo id={task.id} name={task.name} completed={task.completed} />
));

渲染數組:

<ul
  role="list"
  className="todo-list stack-large stack-exception"
  aria-labelledby="list-heading"
>
  {taskList}
</ul>

唯一key

  • 現在React將我們的待辦事項從一個數組中渲染出來,為了正確地渲染它們,React必須記錄並正確地區分它們。
  • React試圖通過自己的猜測來記錄,但是我們可以通過向<Todo />元素傳遞一個key props來幫助它解決這個問題。
  • key是React管理的一個特殊 props ——你不能將key用於任何其他目的。
  • 可以使用nanoid來生成。。。
const taskList = props.tasks.map(task => (
    <Todo
      id={task.id}
      name={task.name}
      completed={task.completed}
      key={task.id}
    />
  )
);
  • 被迭代渲染的每個元素,必須被傳入一個唯一的key

處理事件(handling events)

需求:處理新增Todo事項的表單提交

在React中,我們直接在JSX中的元素上編寫事件處理器:

<button
  type="button"
  onClick={() => alert("hi!")}
>
  Say hi!
</button>

這似乎與最佳實踐建議相悖,建議不要在HTML上使用內聯事件處理程式,但請記住,JSX實際上是JavaScript的一部分。

onClick屬性的意義:

  • 它使得React,在用戶單擊按鈕時運行一個給定的函數。
  • onClick的camelCase非常重要——JSX不會識別onclick(同樣,它已經在JavaScript中用於特定目的,這與標準的onclick處理程式屬性相關但又不同)。
  • 在 JSX 中,所有的瀏覽器事件都遵循這種格式:on + 事件的名稱。

處理表單提交

function handleSubmit(e) {
  e.preventDefault();
  alert('Hello, world!');
}

為<form>元素添加一個onSubmit屬性,並將其值設置為handleSubmit函數:

<form onSubmit={handleSubmit}>

回調(callback) props

需求:在Todo List中,我們需要一個功能:在<Form />中提交新的待辦事項,然後展示在<App />中。

這意味著,我們需要將數據從子組件傳遞到父組件!

  • 在React應用程式中,交互性很少局限於一個組件:一個組件中發生的事件會影響應用程式的其他部分。
  • 我們不能像使用標準props將數據從子組件傳遞到父組件那樣,將數據從父組件傳遞到子組件。

因此,我們無法實現這個需求?

不,我們可以在<App />中編寫一個函數,然後將該函數作為 props 傳遞給<Form />,該函數將從我們的 <Form /> 中獲取一些數據作為輸入。

  • 這個函數prop(function-as-a-prop)被稱為回調(callback)prop。

實現:

  1. 在App()函數中,編寫函數addTask()

    function addTask(name) {
      alert(name);
    }
    
  2. 將addTask函數作為prop傳入<Form />

    <Form addTask={addTask} />
    
  3. 在Form()函數內部的handleSubmit方法中,調用callback prop

    function handleSubmit(e) {
      e.preventDefault();
      props.addTask("Say hello!");
    }
    

State和useState hook

前情提要:前面說到,我們能夠使用callback prop將數據回傳給父組件

需求:如何保存用戶的輸入內容呢?用戶提交了表單之後,如何清空輸入,也就是如何更新內容呢?

總所周知,軟體工程嘛,封裝肯定是越private越好,將數據留在和它最相關的地方。

  • 組件自身擁有的數據,稱為State。
  • State是React的另一個強大工具,因為組件不僅擁有State,而且可以在以後,使用useState hook更新它。
  • 和State不同,prop是只讀的(read-only)!
  • React提供了各種特殊的功能,被稱為hook,允許我們為組件提供新的能力,比如state。

使用React hook前,需要先import:

import React, { useState } from "react";
  • useState()為組件創建一個state,它的唯一參數決定state的初始值。

  • 它返回兩個東西:state和一個稍後可用於更新state的函數。

const [name, setName] = useState('Use hooks!');

文本替換(React 無關)

需求:動態顯示未完成的待辦事項

反單引號字元串拼接+三元表達式

const tasksNoun = taskList.length !== 1 ? 'tasks' : 'task';
const headingText = `${taskList.length} ${tasksNoun} remaining`;

應用:

<h2 id="list-heading">{headingText}</h2>