React 作為一個強大的前端工具,有很多需要熟悉的基本觀念&語法。
這篇文章做一個快速的全面整理,方便工作時,可以隨時翻閱,希望您喜歡。
// In a nutshell, JSX allows us to write HTML in our JS
// JSX can use any valid html tags (i.e. div/span, h1-h6, form/input, etc)
<div>Hello React</div>
// as an expression, JSX can be assigned to variables...
const greeting = <div>Hello React</div>;
const isNewToReact = true;
// ... or can be displayed conditionally
function sayGreeting() {
if (isNewToReact) {
// ... or returned from functions, etc.
return greeting; // displays: Hello React
} else {
return <div>Hi again, React</div>;
}
}
const year = 2020;
// we can insert primitive JS values in curly braces: {}
const greeting = <div>Hello React in {year}</div>;
// trying to insert objects will result in an error
// to write JSX on multiple lines, wrap in parentheses: ()
const greeting = (
// div is the parent element
<div>
{/* h1 and p are child elements */}
<h1>Hello!</h1>
<p>Welcome to React</p>
</div>
);
// 'parents' and 'children' are how we describe JSX elements in relation
// to one another, like we would talk about HTML elements
// Empty div is not <div></div> (HTML), but <div/> (JSX)
<div/>
// A single tag element like input is not <input> (HTML), but <input/> (JSX)
<input name="email" />
// Attributes are written in camelcase for JSX (like JS variables
<button className="submit-button">Submit</button> // not 'class' (HTML)
最基本的 React 應用程式需要三樣東西:
// imports needed if using NPM package; not if from CDN links
import React from "react";
import ReactDOM from "react-dom";
const greeting = <h1>Hello React</h1>;
// ReactDOM.render(root node, mounting point)
ReactDOM.render(greeting, document.getElementById("root"));
import React from "react";
// 1st component type: function component
function Header() {
// function components must be capitalized unlike normal JS functions
// note the capitalized name here: 'Header'
return <h1>Hello React</h1>;
}
// function components with arrow functions are also valid
const Header = () => <h1>Hello React</h1>;
// 2nd component type: class component
// (classes are another type of function)
class Header extends React.Component {
// class components have more boilerplate (with extends and render method)
render() {
return <h1>Hello React</h1>;
}
}
// do we call these function components like normal functions?
// No, to execute them and display the JSX they return...
const Header = () => <h1>Hello React</h1>;
// ...we use them as 'custom' JSX elements
ReactDOM.render(<Header />, document.getElementById("root"));
// renders: <h1>Hello React</h1>
// for example, this Header component can be reused in any app page
// this component shown for the '/' route
function IndexPage() {
return (
<div>
<Header />
<Hero />
<Footer />
</div>
);
}
// shown for the '/about' route
function AboutPage() {
return (
<div>
<Header />
<About />
<Testimonials />
<Footer />
</div>
);
}
// What if we want to pass data to our component from a parent?
// I.e. to pass a user's name to display in our Header?
const username = "John";
// we add custom 'attributes' called props
ReactDOM.render(
<Header username={username} />,
document.getElementById("root")
);
// we called this prop 'username', but can use any valid JS identifier
// props is the object that every component receives as an argument
function Header(props) {
// the props we make on the component (i.e. username)
// become properties on the props object
return <h1>Hello {props.username}</h1>;
}
// Components must ideally be 'pure' functions.
// That is, for every input, we be able to expect the same output
// we cannot do the following with props:
function Header(props) {
// we cannot mutate the props object, we can only read from it
props.username = "Doug";
return <h1>Hello {props.username}</h1>;
}
// But what if we want to modify a prop value that comes in?
// That's where we would use state (see the useState section)
// Can we accept React elements (or components) as props?
// Yes, through a special property on the props object called 'children'
function Layout(props) {
return <div className="container">{props.children}</div>;
}
// The children prop is very useful for when you want the same
// component (such as a Layout component) to wrap all other components:
function IndexPage() {
return (
<Layout>
<Header />
<Hero />
<Footer />
</Layout>
);
}
// different page, but uses same Layout component (thanks to children prop)
function AboutPage() {
return (
<Layout>
<About />
<Footer />
</Layout>
);
}
// if-statements are fine to conditionally show , however...
// ...only ternaries (seen below) allow us to insert these conditionals
// in JSX, however
function Header() {
const isAuthenticated = checkAuth();
return (
<nav>
<Logo />
{/* if isAuth is true, show AuthLinks. If false, Login */}
{isAuthenticated ? <AuthLinks /> : <Login />}
{/* if isAuth is true, show Greeting. If false, nothing. */}
{isAuthenticated && <Greeting />}
</nav>
);
}
Fragments 是用來顯示多個元件的特殊元件,無需向 DOM 添加額外的元素
Fragments 適合用在條件邏輯
// we can improve the logic in the previous example
// if isAuthenticated is true, how do we display both AuthLinks and Greeting?
function Header() {
const isAuthenticated = checkAuth();
return (
<nav>
<Logo />
{/* we can render both components with a fragment */}
{/* fragments are very concise: <> </> */}
{isAuthenticated ? (
<>
<AuthLinks />
<Greeting />
</>
) : (
<Login />
)}
</nav>
);
}
const people = ["John", "Bob", "Fred"];
const peopleList = people.map(person => <p>{person}</p>);
function App() {
const people = ['John', 'Bob', 'Fred'];
// can interpolate returned list of elements in {}
return (
<ul>
{/* we're passing each array element as props */}
{people.map(person => <Person name={person} />}
</ul>
);
}
function Person({ name }) {
// gets 'name' prop using object destructuring
return <p>this person's name is: {name}</p>;
}
迭代的每個 React 元素都需要一個特殊的 key
prop
key 對於 React 來說是必須的,以便能夠追蹤每個正在用 map 迭代的元素
沒有 key,當資料改變時,React 較難弄清楚元素應該如何更新
key 應該是唯一值,才能讓 React 知道如何分辨
function App() {
const people = ['John', 'Bob', 'Fred'];
return (
<ul>
{/* keys need to be primitive values, ideally a generated id */}
{people.map(person => <Person key={person} name={person} />)}
</ul>
);
}
// If you don't have ids with your set of data or unique primitive values,
// you can use the second parameter of .map() to get each elements index
function App() {
const people = ['John', 'Bob', 'Fred'];
return (
<ul>
{/* use array element index for key */}
{people.map((person, i) => <Person key={i} name={person} />)}
</ul>
);
}
// Note: most event handler functions start with 'handle'
function handleToggleTheme() {
// code to toggle app theme
}
// in html, onclick is all lowercase
<button onclick="handleToggleTheme()">
Submit
</button>
// in JSX, onClick is camelcase, like attributes / props
// we also pass a reference to the function with curly braces
<button onClick={handleToggleTheme}>
Submit
</button>
最該先學的 React 事件是 onClick 和 onChange
onClick 處理 JSX 元素上的點擊事件(也就是按鈕動作)
onChange 處理鍵盤事件(也就是打字動作)
function App() {
function handleChange(event) {
// when passing the function to an event handler, like onChange
// we get access to data about the event (an object)
const inputText = event.target.value;
const inputName = event.target.name; // myInput
// we get the text typed in and other data from event.target
}
function handleSubmit() {
// on click doesn't usually need event data
}
return (
<div>
<input type="text" name="myInput" onChange={handleChange} />
<button onClick={handleSubmit}>Submit</button>
</div>
);
}
import React from 'react';
// create state variable
// syntax: const [stateVariable] = React.useState(defaultValue);
function App() {
const [language] = React.useState('javascript');
// we use array destructuring to declare state variable
return <div>I am learning {language}</div>;
}
import React, { useState } from "react";
function App() {
const [language] = useState("javascript");
return <div>I am learning {language}</div>;
}
setter
函數來更新它的狀態function App() {
// the setter function is always the second destructured value
const [language, setLanguage] = React.useState("python");
// the convention for the setter name is 'setStateVariable'
return (
<div>
{/* why use an arrow function here instead onClick={setterFn()} ? */}
<button onClick={() => setLanguage("javascript")}>
Change language to JS
</button>
{/* if not, setLanguage would be called immediately and not on click */}
<p>I am now learning {language}</p>
</div>
);
}
// note that whenever the setter function is called, the state updates,
// and the App component re-renders to display the new state
function App() {
const [language, setLanguage] = React.useState("python");
const [yearsExperience, setYearsExperience] = React.useState(0);
return (
<div>
<button onClick={() => setLanguage("javascript")}>
Change language to JS
</button>
<input
type="number"
value={yearsExperience}
onChange={event => setYearsExperience(event.target.value)}
/>
<p>I am now learning {language}</p>
<p>I have {yearsExperience} years of experience</p>
</div>
);
}
// we have the option to organize state using whatever is the
// most appropriate data type, according to the data we're tracking
function App() {
const [developer, setDeveloper] = React.useState({
language: "",
yearsExperience: 0
});
function handleChangeYearsExperience(event) {
const years = event.target.value;
// we must pass in the previous state object we had with the spread operator
setDeveloper({ ...developer, yearsExperience: years });
}
return (
<div>
{/* no need to get prev state here; we are replacing the entire object */}
<button
onClick={() =>
setDeveloper({
language: "javascript",
yearsExperience: 0
})
}
>
Change language to JS
</button>
{/* we can also pass a reference to the function */}
<input
type="number"
value={developer.yearsExperience}
onChange={handleChangeYearsExperience}
/>
<p>I am now learning {developer.language}</p>
<p>I have {developer.yearsExperience} years of experience</p>
</div>
);
}
function App() {
const [developer, setDeveloper] = React.useState({
language: "",
yearsExperience: 0,
isEmployed: false
});
function handleToggleEmployment(event) {
// we get the previous state variable's value in the parameters
// we can name 'prevState' however we like
setDeveloper(prevState => {
return { ...prevState, isEmployed: !prevState.isEmployed };
// it is essential to return the new state from this function
});
}
return (
<button onClick={handleToggleEmployment}>Toggle Employment Status</button>
);
}
useEffect 讓我們在 function component 中執行副作用。什麼是副作用?
副作用是我們需要接觸外部世界的地方。例如,從 API 獲取資料或操作 DOM。
副作用是可能以不可預測的方式,改變我們元件狀態的操作(導致「副作用」)。
useEffect 接受 callback function(稱為 effect 函數),預設情況下,每次重新渲染時都會運行
useEffect 在我們的元件載入後運行,可以準確在元件的各個生命週期觸發
// what does our code do? Picks a color from the colors array
// and makes it the background color
function App() {
const [colorIndex, setColorIndex] = React.useState(0);
const colors = ["blue", "green", "red", "orange"];
// we are performing a 'side effect' since we are working with an API
// we are working with the DOM, a browser API outside of React
useEffect(() => {
document.body.style.backgroundColor = colors[colorIndex];
});
// whenever state is updated, App re-renders and useEffect runs
function handleChangeIndex() {
const next = colorIndex + 1 === colors.length ? 0 : colorIndex + 1;
setColorIndex(next);
}
return <button onClick={handleChangeIndex}>Change background color</button>;
}
function App() {
...
// now our button doesn't work no matter how many times we click it...
useEffect(() => {
document.body.style.backgroundColor = colors[colorIndex];
}, []);
// the background color is only set once, upon mount
// how do we not have the effect function run for every state update...
// but still have it work whenever the button is clicked?
return (
<button onClick={handleChangeIndex}>
Change background color
</button>
);
}
useEffect 讓我們能夠在 dependencies 陣列的內容改變時,才執行
dependencies 陣列是第二個參數,如果陣列中的任何一個值發生變化,effect function 將再次運行
function App() {
const [colorIndex, setColorIndex] = React.useState(0);
const colors = ["blue", "green", "red", "orange"];
// we add colorIndex to our dependencies array
// when colorIndex changes, useEffect will execute the effect fn again
useEffect(() => {
document.body.style.backgroundColor = colors[colorIndex];
// when we use useEffect, we must think about what state values
// we want our side effect to sync with
}, [colorIndex]);
function handleChangeIndex() {
const next = colorIndex + 1 === colors.length ? 0 : colorIndex + 1;
setColorIndex(next);
}
return <button onClick={handleChangeIndex}>Change background color</button>;
}
function MouseTracker() {
const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 });
React.useEffect(() => {
// .addEventListener() sets up an active listener...
window.addEventListener("mousemove", handleMouseMove);
// ...so when we navigate away from this page, it needs to be
// removed to stop listening. Otherwise, it will try to set
// state in a component that doesn't exist (causing an error)
// We unsubscribe any subscriptions / listeners w/ this 'cleanup function'
return () => {
window.removeEventListener("mousemove", handleMouseMove);
};
}, []);
function handleMouseMove(event) {
setMousePosition({
x: event.pageX,
y: event.pageY
});
}
return (
<div>
<h1>The current mouse position is:</h1>
<p>
X: {mousePosition.x}, Y: {mousePosition.y}
</p>
</div>
);
}
// Note: we could extract the reused logic in the callbacks to
// their own function, but I believe this is more readable
使用 useEffect 撈取資料
請注意,如果要使用更簡潔的 async/awit 語法處理 promise,則需要建立一個單獨的函數(因為 effect callback function 不能是 async)
const endpoint = "https://api.github.com/users/codeartistryio";
// with promises:
function App() {
const [user, setUser] = React.useState(null);
React.useEffect(() => {
// promises work in callback
fetch(endpoint)
.then(response => response.json())
.then(data => setUser(data));
}, []);
}
// with async / await syntax for promise:
function App() {
const [user, setUser] = React.useState(null);
// cannot make useEffect callback function async
React.useEffect(() => {
getUser();
}, []);
// instead, use async / await in separate function, then call
// function back in useEffect
async function getUser() {
const response = await fetch("https://api.github.com/codeartistryio");
const data = await response.json();
setUser(data);
}
}
useCallback 是一個用來提高元件性能的 hook
如果你有一個經常重新渲染的元件,useCallback 可以防止元件內的 callback function 在每次元件重新渲染時都重新創建(導致元件重新運行)
useCallback 僅在其依賴項之一改變時重新運行
// in Timer, we are calculating the date and putting it in state a lot
// this results in a re-render for every state update
// we had a function handleIncrementCount to increment the state 'count'...
function Timer() {
const [time, setTime] = React.useState();
const [count, setCount] = React.useState(0);
// ... but unless we wrap it in useCallback, the function is
// recreated for every single re-render (bad performance hit)
// useCallback hook returns a callback that isn't recreated every time
const inc = React.useCallback(
function handleIncrementCount() {
setCount(prevCount => prevCount + 1);
},
// useCallback accepts a second arg of a dependencies array like useEffect
// useCallback will only run if any dependency changes (here it's 'setCount')
[setCount]
);
React.useEffect(() => {
const timeout = setTimeout(() => {
const currentTime = JSON.stringify(new Date(Date.now()));
setTime(currentTime);
}, 300);
return () => {
clearTimeout(timeout);
};
}, [time]);
return (
<div>
<p>The current time is: {time}</p>
<p>Count: {count}</p>
<button onClick={inc}>+</button>
</div>
);
}
useMemo 和 useCallback 非常相似,都是為了提高效能。但就不是為了暫存 callback function,而是為了暫存耗費效能的運算結果
useMemo 允許我們「記憶」已經為某些輸入進行了昂貴計算的結果(之後就不用重新計算了)
useMemo 從計算中回傳一個值,而不是 callback 函數(但可以是一個函數)
// useMemo is useful when we need a lot of computing resources
// to perform an operation, but don't want to repeat it on each re-render
function App() {
// state to select a word in 'words' array below
const [wordIndex, setWordIndex] = useState(0);
// state for counter
const [count, setCount] = useState(0);
// words we'll use to calculate letter count
const words = ["i", "am", "learning", "react"];
const word = words[wordIndex];
function getLetterCount(word) {
// we mimic expensive calculation with a very long (unnecessary) loop
let i = 0;
while (i < 1000000) i++;
return word.length;
}
// Memoize expensive function to return previous value if input was the same
// only perform calculation if new word without a cached value
const letterCount = React.useMemo(() => getLetterCount(word), [word]);
// if calculation was done without useMemo, like so:
// const letterCount = getLetterCount(word);
// there would be a delay in updating the counter
// we would have to wait for the expensive function to finish
function handleChangeIndex() {
// flip from one word in the array to the next
const next = wordIndex + 1 === words.length ? 0 : wordIndex + 1;
setWordIndex(next);
}
return (
<div>
<p>
{word} has {letterCount} letters
</p>
<button onClick={handleChangeIndex}>Next word</button>
<p>Counter: {count}</p>
<button onClick={() => setCount(count + 1)}>+</button>
</div>
);
}
Refs 是所有 React 組件都可用的特殊屬性。允許我們在元件安裝時,創建針對特定元素/元件的引用
useRef 允許我們輕鬆使用 React refs
我們在元件開頭呼叫 useRef,並將回傳值附加到元素的 ref 屬性來引用它
一旦我們創建了一個引用,就可以用它來改變元素的屬性,或者可以呼叫該元素上的任何可用方法(比如用 .focus() 來聚焦)
function App() {
const [query, setQuery] = React.useState("react hooks");
// we can pass useRef a default value
// we don't need it here, so we pass in null to ref an empty object
const searchInput = useRef(null);
function handleClearSearch() {
// current references the text input once App mounts
searchInput.current.value = "";
// useRef can store basically any value in its .current property
searchInput.current.focus();
}
return (
<form>
<input
type="text"
onChange={event => setQuery(event.target.value)}
ref={searchInput}
/>
<button type="submit">Search</button>
<button type="button" onClick={handleClearSearch}>
Clear
</button>
</form>
);
}
// Context helps us avoid creating multiple duplicate props
// This pattern is also called props drilling:
function App() {
// we want to pass user data down to Header
const [user] = React.useState({ name: "Fred" });
return (
// first 'user' prop
<Main user={user} />
);
}
const Main = ({ user }) => (
<>
{/* second 'user' prop */}
<Header user={user} />
<div>Main app content...</div>
</>
);
const Header = ({ user }) => <header>Welcome, {user.name}!</header>;
// Here is the previous example rewritten with Context
// First we create context, where we can pass in default values
const UserContext = React.createContext();
// we call this 'UserContext' because that's what data we're passing down
function App() {
// we want to pass user data down to Header
const [user] = React.useState({ name: "Fred" });
return (
{/* we wrap the parent component with the provider property */}
{/* we pass data down the computer tree w/ value prop */}
<UserContext.Provider value={user}>
<Main />
</UserContext.Provider>
);
}
const Main = () => (
<>
<Header />
<div>Main app content...</div>
</>
);
// we can remove the two 'user' props, we can just use consumer
// to consume the data where we need it
const Header = () => (
{/* we use this pattern called render props to get access to the data*/}
<UserContext.Consumer>
{user => <header>Welcome, {user.name}!</header>}
</UserContext.Consumer>
);
const Header = () => {
// we pass in the entire context object to consume it
const user = React.useContext(UserContext);
// and we can remove the Consumer tags
return <header>Welcome, {user.name}!</header>;
};
// let's say this reducer manages user state in our app:
function reducer(state, action) {
// reducers often use a switch statement to update state
// in one way or another based on the action's type property
switch (action.type) {
// if action.type has the string 'LOGIN' on it
case "LOGIN":
// we get data from the payload object on action
return { username: action.payload.username, isAuth: true };
case "SIGNOUT":
return { username: "", isAuth: false };
default:
// if no case matches, return previous state
return state;
}
}
Reducers 是一種強大的狀態管理模式,用於流行的 Redux 狀態管理套件(這套很常與 React 一起使用)
與 useState(用於本地元件狀態)相比,Reducers 可以通過 useReducer hook 在 React 中使用,以便管理我們應用程序的狀態
useReducer 可以與 useContext 配對來管理資料並輕鬆地將資料傳遞給元件
useReducer + useContext 可以當成應用程式的整套狀態管理系統
const initialState = { username: "", isAuth: false };
function reducer(state, action) {
switch (action.type) {
case "LOGIN":
return { username: action.payload.username, isAuth: true };
case "SIGNOUT":
// could also spread in initialState here
return { username: "", isAuth: false };
default:
return state;
}
}
function App() {
// useReducer requires a reducer function to use and an initialState
const [state, dispatch] = useReducer(reducer, initialState);
// we get the current result of the reducer on 'state'
// we use dispatch to 'dispatch' actions, to run our reducer
// with the data it needs (the action object)
function handleLogin() {
dispatch({ type: "LOGIN", payload: { username: "Ted" } });
}
function handleSignout() {
dispatch({ type: "SIGNOUT" });
}
return (
<>
Current user: {state.username}, isAuthenticated: {state.isAuth}
<button onClick={handleLogin}>Login</button>
<button onClick={handleSignout}>Signout</button>
</>
);
}
創建 hook 就能輕鬆在元件之間重用某些行為
hook 是一種比以前的 class component 更容易理解的模式,例如 higher-order components 或者 render props
根據需要,隨時可以自創一些 hook
// here's a custom hook that is used to fetch data from an API
function useAPI(endpoint) {
const [value, setValue] = React.useState([]);
React.useEffect(() => {
getData();
}, []);
async function getData() {
const response = await fetch(endpoint);
const data = await response.json();
setValue(data);
};
return value;
};
// this is a working example! try it yourself (i.e. in codesandbox.io)
function App() {
const todos = useAPI("https://todos-dsequjaojf.now.sh/todos");
return (
<ul>
{todos.map(todo => <li key={todo.id}>{todo.text}</li>)}
</ul>
);
}
使用 React hooks 有兩個核心規則,要遵守才能正常運作:
Hooks 不能放在條件、迴圈或嵌套函數中
Hooks 不能放在普通的 JavaScript 函數或 class component 中
function checkAuth() {
// Rule 2 Violated! Hooks cannot be used in normal functions, only components
React.useEffect(() => {
getUser();
}, []);
}
function App() {
// this is the only validly executed hook in this component
const [user, setUser] = React.useState(null);
// Rule 1 violated! Hooks cannot be used within conditionals (or loops)
if (!user) {
React.useEffect(() => {
setUser({ isAuth: false });
// if you want to conditionally execute an effect, use the
// dependencies array for useEffect
}, []);
}
checkAuth();
// Rule 1 violated! Hooks cannot be used in nested functions
return <div onClick={() => React.useMemo(() => doStuff(), [])}>Our app</div>;
}
以上是快速整理,希望對您有幫助!