React.js 101

By 增廣建文

Agenda

  • What's React.js?
  • Components
  • Hooks
  • React Router
  • Redux
  • React UI Frameworks

What's React?


React.js是由facebook開發的前端框架,目前是三大框架中熱度最高的

Create First Project


為了節省自己創建一堆目錄以及安裝套件的時間,通常會使用叫create-react-app的套件

由於他不是一個經常會使用的套件,所以可以直接用npx去執行

npx create-react-app my-app  # npx create-react-app .
cd my-app
npm start
  • my-app指的是他會創建的資料夾名稱,要直接在當前資料夾可以用.
  • 啟動的script以及必備套件都已經設定好
  • npm start可以看到一個正在轉的react logo
  • npm run build可以把網站打包成靜態的

Understand Files and Folders


可以看到由create-react-app創建的檔案結構大致會如下

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
d-----       2022/7/24  下午 10:57                node_modules
d-----       2022/7/24  下午 10:57                public
d-----       2022/7/24  下午 10:57                src
-a----       2022/7/24  下午 10:57            310 .gitignore
-a----       2022/7/24  下午 10:57        1177487 package-lock.json
-a----       2022/7/24  下午 10:57            811 package.json
-a----       2022/7/24  下午 10:57           3359 README.md
  • src內放的會是我們寫的js/ jsx code
  • public則放一些靜態的檔案,像是html與圖片

React.js Entrypoint


React網頁的進入點會在public/index.html,他會去呼叫src/index.js

index.js又會去使用App.js當作最上層的component

底下是src資料夾下有哪些預設的檔案

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
-a----       2022/7/24  下午 10:57            564 App.css
-a----       2022/7/24  下午 10:57            528 App.js
-a----       2022/7/24  下午 10:57            246 App.test.js
-a----       2022/7/24  下午 10:57            366 index.css
-a----       2022/7/24  下午 10:57            535 index.js
-a----       2022/7/24  下午 10:57           2632 logo.svg
-a----       2022/7/24  下午 10:57            362 reportWebVitals.js
-a----       2022/7/24  下午 10:57            241 setupTests.js

What's Component?


三大前端框架的使用方式和純用JS或是Jquery撰寫網頁時有個明顯的差異

那就是他們都用上了component的概念,和過往每個頁面都需要獨立html不同

每當使用者和網頁互動,只要render畫面上有異動的部分

Play with Default App Component


可以把用不到的像是logo等等給移除,react支援hot-reload所以改的同時就能看到效果

import './App.css';

function App() {
  return (
    <div className="App">
      <header className="App-header">
        <a className="App-link"  href="https://reactjs.org"
          target="_blank" rel="noopener noreferrer">
          Hello World
        </a>
      </header>
    </div>
  );
}

export default App;

What's JSX?


JSX就是前面看到的JS和html tag混用後的產物

因為JSX 語法是特殊的語法擴充,所以一般的瀏覽器是沒辦法辨識 JSX 語法的

需要像是 Babel 等類似工具來轉譯成瀏覽器可以理解的 JavaScript

在使用create-react-app創建專案時就已經把babel設定好了

function PersonOne() {
  return (
    <div>
      <h1>Bill</h1>
      <p>Your Age: 26</p>
    </div>
  );
}

JSX - Combine Content with JS Variable

function formatName(user) {
  return user.firstName+ ' ' + user.lastName;
}

const user = {
  firstName: 'Harper',
  lastName: 'Perez'
};

function getGreeting(user) {
  if (user) {
    return <h1>Hello, {formatName(user)}!</h1>;
  }
  return <h1>Hello, Stranger.</h1>;
}

const className = 'container'
const content = 'Cool! This is JSX syntax'

const element = <div className={className}>{content}</div>

JSX - Fragment


Fragment是非常實用的小技巧,可以省去return時會需要的最外層<div>

import { Fragment } from "react";

function PersonOne() {
  return (
    <Fragment>
      <h1>Bill</h1>
      <p>Your Age: 26</p>
    </Fragment>
  );
}

Fragment也可以縮寫成<> </>

Functional Component

現在啟動專案時給的範例就是functional component

function App() {
  return (
    <div className="App">
      <img src={logo} className="App-logo" alt="logo" />
      <p>This is my first React App!</p>
    </div>
  );
}

要用時也非常方便

import App from "./App";

ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
);

Class Components


早期多會使用class componet來實作,不過像在通常會轉向更好學的React Hooks

// car.js
class Car extends React.Component {
  constructor() {
    super();
    this.state = {color: "red"};
  }
  render() {
    return <h2>I am a Car!</h2>;
  }
}

// app.js
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<Car />);

Child Component


看完前面單一個component的運作方式後,接下來要講比較複雜的child component概念

現在主流的前端框架邏輯都是將畫面上各個元素拆分成不同component來去實作

有時候會遇到一個比較大的部分就會需要再去切分成多個component

像是如果你有一個比較複雜的Nav-Bar,就可以把其中的元素給拆分開

接下來我們會示範最簡單的用法,在現有的App component下建立一個component

這樣App component實際上就變成了parent

Add a Child Component


Component的檔案與function命名會有以下的限制

  • Component 的第一個字母一定要大寫
  • Component 的名稱也必須和該函式 Function 同名
// App2.js
function App2() {
    return (
      <div>
          <p>I am child component!</p>
      </div>
    );
  }
  
export default App2;

只要在App.js裡面加上<App2 />就可以了

React Testing


預設設定好的test framework也是jest,之後會教更好用的cypress

import { render, screen } from '@testing-library/react';
import App from './App';

test('renders learn react link', () => {
  render(<App />);
  const linkElement = screen.getAllByText(/I am/i);  // screen.getAllByText(/I am/i)[0]
  expect(linkElement).toBeInTheDocument();
});

React Testing Results


由於用的是jest,自然輸出的格式也會看起來差不多

PASS  src/App.test.js
  √ renders learn react link (7 ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        0.463 s, estimated 1 s
Ran all test suites.

Props


在不同component之間傳遞資料,主要是由父傳給子(接受到的props是read-only的)

// App2.js
function App2(props) {
  return <h1>Hello, {props.name}</h1>;
}

在parent component中的呼叫方式

// App1.js
let props = {
  name: 'Wade'
}

// inside return()
<App2 name='John' />
<App2 {...props}/>

State with Class Component

處理的是component內部的狀態,當props或是state更新都能觸發元件re-render

// clock.js
import React from 'react'

class Clock extends React.Component {
  constructor(props) {
    super(props);
    this.state = {date: new Date()};
  }
  render() {
    return (
      <div>
        <h1>Hello, world!</h1>
        <h2>It is {this.state.date.toLocaleTimeString()}.</h2>
      </div>
    );
  }
}
export default Clock

React Lifecycle


早期講到class component + state就必須提到的react的lifecycle

不然就會像前一頁的範例一樣,時鐘不會自動更新時間

不過因為之後都用Hook所以就先不提

Hooks - useState

Class Component外另一種更方便管理state和lifecycle的寫法

useState是最常用到的hook,可以存跟改變state

import { useState } from "react";

function FavoriteColor() {
  const [color, setColor] = useState("red");

  return (
    <>
      <h1>My favorite color is {color}!</h1>
      <button
        type="button"
        onClick={() => setColor("blue")}
      >Blue</button>
    </>
  )
}

Hooks - useEffect 1/2

useEffect是設計用來處理side effect (在render後才做事),包含call API等

useEffect會吃兩個參數,第一個是函式,第二個則是dependency array

const Counter = () => {
  const [count, setCount] = useState(0) 
  useEffect(()=>{
    // 第一次 render 後會執行以外,每當 count 變數的值有變動時執行
     console.log('No.3') 
  }, [count])

  const increaseHandler = () => {
    setCount(prev => prev + 1) 
  }

  return (
    <div>
      <span>{count}</span>
      <button onClick={increaseHandler}></button>
    </div>
)};

Hooks - useEffect 2/2

useEffect根據帶入的參數不同,總共會產生四種不同的情境

import { useEffect } from "react";

useEffect(()=>{
    // 每一次 render 後會執行
})

useEffect(()=>{
    // 只有在第一次 render 後執行
}, [])

useEffect(()=>{
    // 第一次 render 後會執行以外,每當 count 變數的值有變動時執行
}, [count])

useEffect(()=>{
    // 第一次 render 後執行以外,每當 count 變數的值有變動時執行,但會先等 return 的函式執行完後才會執行這裡
    return ()=>{
    // 第一次 render 不執行,第二次重新 render 後先執行這個函式
    }
}, [count])

用Hook Call API並存資料 - axios


axios是一個常見能夠發http request的套件,需要安裝npm i axios

import axios from 'axios'

const Home = props => {
    useEffect(() => {
      axios.get('http://localhost:8000/api/hello')
        .then(res => setState(res.data))
    }, [])

    const [state, setState] = useState('')

  return(
    <div>
      Home
      <p>{state}</p>
    </div>
 )
};

用Hook Call API並存資料 - fetch


透過useEffect在component剛load時去fetch,然後用setState存資料

const Home = props => {
    const [state, setState] = useState('')

    useEffect(() => {
      fetch('http://localhost:8000/api/hello', {"method": "GET"})
        .then((response) => response.json())
        .then(res => setState(res.data))
    }, [])

  return(
    <div>
      Home
      <p>{state}</p>
    </div>
 )
};

Fetch API and Auto Refresh


使用useState + useEffect去fetch API要小心無限迴圈

useEffect(()=>{
    
    getPosts()
    const interval=setInterval(()=>{
      getPosts()
     },10000)
       
       
     return()=>clearInterval(interval)
},[])

Lift State 1/2


讓同parent的child component可以用到同一個state

實際功用就是將其中一個child component的state變成parent component的props

// App.js
import React from "react";

export default function App() {
  const [todos, setTodos] = React.useState(["item 1", "item 2", "item 3"]);

  return (
    <>
      <TodoCount todos={todos} />
      <TodoList todos={todos} />
      <AddTodo setTodos={setTodos} />
    </>
  );
}

Lift State 2/2


Child component會用到來在parent component的useState去更新其state

// AddTodo.js
function AddTodo({ setTodos }) {
  function handleSubmit(event) {
    event.preventDefault();
    const todo = event.target.elements.todo.value;
    setTodos(prevTodos => [...prevTodos, todo]);
  }

  return (
    <form onSubmit={handleSubmit}>
      <input type="text" id="todo" />
      <button type="submit">Add Todo</button>
    </form>
  );

useRef 1/3


useRef讓我們可以透過hook去修改DOM node的值

// App.js
    function App() {

      return (
        <div>
          <button>To top</button>
          <button>To bottom</button>
          <p className='box'>
              這裡要放超多文字讓滾輪出現
          </p>
          <button onClick={toTop}>To top</button>
          <button onClick={toBottom}>To bottom</button>
        </div>
      )
    }

useRef 2/3


透過設定css讓文字不用滿版就會有滾輪出現

/* App.css */
.box {
    width: 300px;
    height: 300px;
    overflow: auto;
    padding: 10px;
    border: 1px solid black;
  }

useRef 3/3


useRef會建立一個mutable的物件,之後要把mutable物件給放到要取的物件

const boxContainer = React.useRef(null)

const toTop = () => {
  boxContainer.current.scrollTop = 0
}

const toBottom = () => {
  // const { current: boxEl } = boxContainer
  const boxEl = boxContainer.current
  boxEl.scrollTop = boxEl.scrollHeight
}

要把return的element上加上ref的參數

<p className='box' ref={boxContainer}></p>

useReducer


useReducer和之前介紹過的useState較為接近,也和未來要介紹的Redux比較有關

const [state, setState] = useReducer((state, newState)=>{
  console.log(newState) // 印出 3 
}, initialValue)

setState(3)

如果去改寫在useState寫的範例就會變成

function countReducer(state, newState){
  return newState
}

const [count, setCount] = React.useReducer(countReducer, 0)

可以看到實際差異就是第一個參數要帶的是自訂的callback function

useReducer - dispatch

前面示範的改法雖然也會work,但通常會習慣使用dispatch的寫法

function countReducer(state, action){
  switch (action.type) {
    case 'increment': 
      return state + 1;
    case 'decrement': 
      return state - 1;
    default: 
      throw new Error(`no ${action.type} type in counterReducer`)
  }
}

const [count, dispatch] = React.useReducer(countReducer, 0)

const increaseHandler = () => {
  dispatch({type: 'increment'})
}

修改完就可以去拿state.count

useReducer vs useState 1/3

雖然useReducer的寫法相較於useState複雜許多,但在特定場合下卻會比較好用

而這個特定場合就是指當一個state會去影響到另外一個state(同component)

我們這裡舉的例子是有pagination + sorting需求時,先定義一下reducer

function reducer(state, action){
  switch (action.type) {
    case 'change sort': 
      const nextType = state.sortBy.type === 'desc' ? 'asc' : 'desc'
      const nextSortBy = { ...action.sortBy, type: nextType }
      return { ...state, page: 1, sortBy: nextSortBy } 
    
    case 'change page': 
      return { ...state, page: action.page  }

    default: 
      throw new Error(`no ${action.type} type in reducer`)
  }
}

useReducer vs useState 2/3

component中的其他部分

const App = () => {
  const [posts, setPosts] = useState([])
  const [payload, dispatch] = useReducer(reducer, {
    page: 1,
    sortBy: {
      category: 'date',
      type: 'desc'
    }
  })
 
  function sortHandler(e){
    const category = e.target.id
   
    dispatch({type: 'change sort', sortBy: { category } }) 
  }
  
  function pageHandler(e){
    dispatch({type: 'change page', page: e.target.id})
  }
  
  useEffect(() => {
    // fetch api and setPost
    
  }, [page, sortBy]);
  
  // return UI ..... 
}

useReducer vs useState 3/3

這種情況如果選用useState就會把handler搞得很複雜

const App = () => {
  const [posts, setPosts] = useState([])
  const [page, setPage] = useState(1);
  const [sortBy, setSortBy] = useState({
    category: 'date',
    type: 'desc'
  });
 
  function sortHandler(e){
    const sortCategory = e.target.id
   
    setPage(1)
    setSortBy(prev => {
      const nextType = prev.type === 'desc' ? 'asc' : 'desc'
    
      return {
        ...prev,
        category: sortCategory,
        type: nextType
      }
    })
  }
  
  function pageHandler(e){
    setPage(e.target.id)
  }
}

useContext & createContext 1/6


之前提Lift state的時候是假定只要將state給上層的component

假設這時候要給上上/上上上層就會變得很難寫,可以改用context來協助傳遞state

// 建立context object
const Context = createContext(defaultValue)

// Context長的樣子
<Context.Provider value={/* some value */}>
  {children}
</Context.Provider>

// 取得context中的value
const value = useContext(Context)

useContext & createContext 2/6


由於這裡會牽涉的範例component過多,詳細可以參考下方的連結

這裡會先建context provider

// ThemeContext.js
const ThemeContext = createContext('light')

export function ThemeContextProvider({children}) {
  const [darkTheme, setDarkTheme] = useState(false)
  const theme = darkTheme ? 'dark' : 'light'
  
  return (
    <ThemeContext.Provider value={[theme, setDarkTheme]}>
        <App2 />
        {children}
    </ThemeContext.Provider>
  )
}

useContext & createContext 3/6


再來要去建useContext

// ThemeContext.js
export function useThemeContext() {
  const contextValue = useContext(ThemeContext)
  
  if (!contextValue) {
    throw new Error('you should wrap the component in theme context provider')
  }

  return contextValue
}

useContext & createContext 4/6


會用到context的component都必須被context provider給包住

// App.js
import { Header } from './components/Header'
import { Body } from './components/Body'
import { Footer } from './components/Footer'
import { ThemeContextProvider } from './components/ThemeContext'

function App() {
  return (
    <ThemeContextProvider>
      <Header />
      <Body />
      <Footer />
    </ThemeContextProvider>
  )
}

useContext & createContext 5/6


會取到theme value的body

// body.js
import { useThemeContext } from './components/ThemeContext'

function Body() {
  const [theme] = useThemeContext()
  
  return (
    <div>
      {theme}
    </div>
  )
}

useContext & createContext 6/6


要設定theme value的button

// button.js
import { useThemeContext } from './components/ThemeContext'

function Button() {
  const [theme, setDarkTheme] = useThemeContext()

  return (
    <button onClick={() => { setDarkTheme(prev => !prev) }}>{theme}</button>
  )
}

useContext + API


如果不是在最上層先call API來存資料,就可以用來把所有資料存到最上層

useCallback


useCallback可以幫我們盡量的減少一些不必要的render,來增強系統的表現

useCallback和useEffect接收的參數是一樣的,就是callback function + dependency array

const memorizeCallback = useCallback(() => {
  // do something
}, [])

useCallback在re-render時會根據dependency array決定要不要生成一個新的function

而useCallback回傳的memoized callback就是負責紀錄function的物件

useCallback Example 1/2

在不使用useCallback時只有第一次render時會return True,之後就只會return False

  function App() {
    const [count, setCount] = React.useState(0)
    const callbackRef = React.useRef()

    const test = () => {
      console.log('test callback')
    }

    React.useEffect(() => {
      callbackRef.current = test
    }, [])

    React.useEffect(() => {
      console.log(test === callbackRef.current)
    }, [count])

    return (
      <div>
        <button onClick={() => setCount(count + 1)}>{count}</button>
      </div>
    )
  }

useCallback Example 2/2


如果把test的function改成用useCallback寫就會得到不一樣的效果

  const test = React.useCallback(() => {
    console.log('test callback')
  }, [])

因為每次使用到的其實都是相同的function實體

When to use useCallback? 1/2


useCallback最適合的使用時機是某個function會很常被useEffect等hook使用到時

以call API為例子,我們可能會把共用到的部分以function的形式放在hook外

function fetchData(query){
  const baseUrl = `xxxxxxxx?${query}`
}

React.useEffect(()=> {
  fetchData('JavaScript')
}, [fetchData])

React.useEffect(()=> {
  fetchData('React')
}, [fetchData])

這時候只要每次re-render fetchData就都會是新的實體

When to use useCallback? 2/2


而解決的辦法就是透過useCallback來儲存fetchData,可以避免每次re-render都產生新的實體

const fetchData = React.useCallback((query) => {
  const baseUrl = `xxxxxxxx?${query}`
}, [])

React.useEffect(()=> {
  fetchData('JavaScript')
}, [fetchData])

React.useEffect(()=> {
  fetchData('React')
}, [fetchData])

useCallback + useState


如果再把前面的範例搭配上useState就會變成

const [query, setQuery] = React.useState('JavaScript')

const fetchData = React.useCallback((query) => {
  const baseUrl = `xxxxxxxx?${query}`
}, [query])

React.useEffect(()=> {
  fetchData()
}, [fetchData])

useCallback + React.memo

useMemo

useCallback vs useMmeo

useLayoutEffect

useImperativeHandle

React.forwardRef

Custom Hook

React Query

useDebugValue

React Router

Part 3.

React Router v6


可以去模擬靜態網頁的路由關係,需要先安裝npm install react-router-dom@6

// App.jsx
import { BrowserRouter as Router, Routes, Route, Link } from 'react-router-dom'

function App () {
    return (
      <>
        <Routes>
          <Route element={<Home />} path={'/'}></Route>
          <Route element={<List />} path='/list'></Route>
          <Route element={<Id />} path='/id/:id'></Route>
        </Routes>
      </>
    )
}

routes一定要放在return內才會生效

Setup Browser Router in Index.js


除了安裝套件並寫routes外還要去index.js中設定啟用模擬的router

// src/index.js
import { BrowserRouter } from "react-router-dom";

ReactDOM.render(
  <React.StrictMode>
    <BrowserRouter>
      <App />
    </BrowserRouter>
  </React.StrictMode>,
  document.getElementById("root")
);

Use Routes in Component


除了以Link的方式轉向外也能直接在網址列輸入

// App.jsx
import { BrowserRouter as Router, Routes, Route, Link } from 'react-router-dom'

function App() {
  return (
    <div className="App">
        <nav>
          <Link to="/home">Home</Link>
        </nav>
    </div>
  );
}

React Router - History


可以用來回到上一頁

import { useHistory } from "react-router-dom";

export const Profile = () => {
    let history = useHistory();
    console.log(history);
    return (
        <div>
            <button onClick={() => history.goBack()}>Back</button>
            <button onClick={() => history.push("/")}>Home</button>
            <section>
                <p>profile page</p>
            </section>
        </div>
    );
};

React Router - Navigate


和Link一樣主要用來實現路由的變換

import { useNavigate } from "react-router-dom";

const ABOUT = () => {
    const navigate = useNavigate()
    const onClick = () => {
        navigate('/')
    }
    return (
        <div>
            <button onClick={onClick}>BACK</button>
        </div>
    )
}

React Router - 抓網址


React Router還有提供其他實用的hook或是function

import { useLocation, useParams, useSearchParams } from "react-router-dom";

const location = useLocation()

const params = useParams()

const [getParams, setParam] = useSearchParams()
const name = getParams.getAll('name')

React-loadable

可以幫助切分程式碼,在會被用到的時候才讀取,這樣能夠增加網頁的效能

首先需要安裝套件 npm install --save react-loadable

// App.js
import Loadable from 'react-loadable';

// 撰寫如果網頁載入較慢時要render的component
const Loading = () => {
	return <div>Loading......</div>
};

// 用react-loader包裝的Home Component
const Home = Loadable({
    // 要load的component,用import的方式插入componenr
	loader() {
		return import('./Home');
	},
    // 如果loading較慢時,要render的component
	loading: Loading,
});

React-loadable + React Router


把前面的code再加上react router

function App () {
  return(
  <Router>
    <div>
      <Route path="/" exact component={Home} />
      <Route path="/About" component={About} />
      <Route path="/Contact" component={Contact} />
      <Footer />
    </div>
  </Router>
);
}

Error Handling and Timeout


確保當連線失敗或是發生錯誤時都能給予正確的提示

function Loading(props) {
  if (props.error) {
    return <div>Error! <button onClick={ props.retry }>Retry</button></div>;
  } else if (props.timedOut) {
    return <div>Taking a long time... <button onClick={ props.retry }>Retry</button></div>;
  } else if (props.pastDelay) {
    return <div>Loading...</div>;
  } else {
    return null;
  }
}

Loadable({
  loader: () => import('./components/Bar'),
  loading: Loading,
  timeout: 10000, // 10 seconds
});

Load Component and Call API at the same time


Loader.Map在loading時可以透過function做多件事

Loadable.Map({
  loader: {
    Bar: () => import('./Bar'),
    i18n: () => fetch('./i18n/bar.json').then(res => res.json()),
  },
  render(loaded, props) {
    let Bar = loaded.Bar.default;
    let i18n = loaded.i18n;
    return <Bar {...props} i18n={i18n}/>;
  },
});

Preload

在背景先將component給加載好

const LoadableBar = Loadable({
  loader: () => import('./Bar'),
  loading: Loading,
});

function App () {
  onMouseOver = () => {
    LoadableBar.preload();
  };

  return (
    <div>
      <button
        onMouseOver={this.onMouseOver}>
        Show Bar
      </button>
    </div>
  )
}

Static Router

如果要以靜態網頁的形式去serve react專案的話,就得把原本的react-router給修正

因為網頁的內容實際上會是由backend server先render好

// App.js

import { StaticRouter } from "react-router-dom/server"

const location = {
  pathname: '/about', // 路徑,這是唯一必要的
  search: '?bar=1', // query 的部份
  hash: '#baz', // hash 的部份
  state: {}, // 物件裡可以傳任何東西
}

<StaticRouter location={location}>
    <Routes>
      <Route path="/about" component={About} />
    </Routes>
</StaticRouter>

BrowserRouter vs StaticRouter


BrowserRouter是由前端瀏覽器渲染的,而StaticRouter則是由後端處理

改用StaticRouter的話,記得要把index.js中的BrowserRouter給移除

// <BrowserRouter>
      <App />
// </BrowserRouter>

React Redux

Part 4.

Redux


可以去存跨component的state,也需要先安裝npm install @reduxjs/toolkit

由於redux的整體非常複雜,所以官方會建議從使用包好的toolkit來下手

如果在使用create-react-app時就下定決心要用到redux的話可以下

# Redux + Plain JS template
$ npx create-react-app my-app --template redux

# Redux + TypeScript template
$ npx create-react-app my-app --template redux-typescript

Redux相關的基本概念


核心概念是當有action在UI上被發出後得靠dispatch才能去更動store內的state

Terms and Functions in Redux-toolkit


常見的會有以下:

  • store
  • dispatch
  • reducer
  • action
  • selector

Use Redux in Existing Project 1/4


為了要在react專案中用redux-toolkit,這裡還會需要再安裝一個叫做react-redux的套件

首先要建立一個檔案來創建並管理store

// store.js
import { configureStore } from "@reduxjs/toolkit"
import counterReducer from './counterSlice';

export const store = configureStore({
  reducer: {
    counter: counterReducer,
  }
})

Use Redux in Existing Project 2/4


再來還得到App.js去做設定,這樣component才可以使用到store/reducer

// App.js
import { Provider } from 'react-redux';
import { store } from './store';
import Container from './Container';

// 在component內的最外層加上
  <Provider store={store}>
    <Container />
  </Provider>

這樣設定完才能讓所有的component都可以用上來自redux的global state

Use Redux in Existing Project 3/4

透過 slice 可以產生 reducer 和 action creators

// counterSlice.js
import { createSlice } from '@reduxjs/toolkit'
export const counterSlice = createSlice({
  name: 'counter',
  initialState: { value:0 },
  reducers: {
    increment: (state) => ({
      value: state.value + 1,
    }),
    decrement: (state) => ({
      value: state.value - 1,
    }),
  },
})
// action creators
export const { increment, decrement } = counterSlice.actions
// reducer
export default counterSlice.reducer

Use Redux in Existing Project 4/4

最後用個container component當作範例來使用store中的state

// container.js
import { useDispatch, useSelector } from 'react-redux'
import { increment, decrement } from './counterSlice';

const Container = () => {
  const dispatch = useDispatch();
  const count = useSelector((state) => state.counter.value)

  const handleIncrement = () => {
    dispatch(increment())
  }

  const handleDecrement = () => {
    dispatch(decrement())
  }

  return (
    <div>
      <h1>Count: {count}</h1>
      <button onClick={handleDecrement}>Decrement</button>
      <button onClick={handleIncrement}>Increment</button>
  </div>
  )
}
export default Container

useSelector


useSelector 會將前一個結果與當前的結果進行比較

如果不同就會取得新值並強制重新render component

const counter = useSelector(state => state.counter)

Create AsyncThunk


AsyncThunk是redux提供專門用來處理非同步事件的,尤其像是fetch API這類很花時間的

import { createAsyncThunk } from '@reduxjs/toolkit'

export const fetchPosts = createAsyncThunk('posts/fetchByIdStatus', async (Uid, thunkAPI) => {
    const body = {"Uid": Uid}
    let res = await fetch('http://localhost:8000/post/view', {
        method: "POST",
        body: JSON.stringify(body)
    })
    res = await res.json()
    if (res !== null)
        return res.posts
    else
        return []
})

Handle AsyncThunk in Reducer


由於預設的reducer無法直接處理異步事件,這裡要用extraReducer來處理不同狀態

export const pageSlice = createSlice({
  name: 'page',
  initialState: {
    posts: [],
  },
  reducers: {
    // ...
  },
  extraReducers: (builder) => {
    builder.addCase(fetchPosts.fulfilled, (state, action) => {
      state.posts = action.payload  //  return { posts: action.payload }
    });
  },
})

React + UI Framework

Part 5.

React搭配CSS

通常檔案名稱取的和component一致,像是app.js搭配app.css

/* App.css */
.App {
  text-align: center;
}
// App.js
import './App.css';

function App() {
  return (
    <div className="App">
      React 101
    </div>
  );
}

export default App;

React.js UI Library

  • React-Bootstrap
  • Ant Design of React
  • MUI
  • React Toolbox
  • PrimeReact

React-Bootstrap

React-Bootstrap就是把boostrap包裝成方便React使用的版本,但是版本號碼會落後些

要使用的話需要安裝兩個套件 npm install react-bootstrap bootstrap

import Button from 'react-bootstrap/Button';
function Demo () {
  return (
    <Stack direction="horizontal" gap={2}>
      <Button as="a" variant="primary">
        Button as link
      </Button>
      <Button as="a" variant="success">
        Button as link
      </Button>
    </Stack>
  )
}

CSS要在index.js中設定 import 'bootstrap/dist/css/bootstrap.min.css';

PrimeReact


PrimeReact是專為React而生的UI套件,可以透過npm i primereact primeicons安裝

import { Dialog } from 'primereact/dialog';
import { Button } from 'primereact/button';

function Demo () {
  return (
    <Dialog visible={state} onHide={() => setState(false)}>
    // content
    </Dialog>

    <Button label="Show" onClick={() => setState(true)} />
  )
}

PrimeReact - Setup CSS


CSS的部分要額外到index.js去設定

import "primereact/resources/themes/lara-light-indigo/theme.css";  //theme
import "primereact/resources/primereact.min.css";                  //core css
import "primeicons/primeicons.css";                                //icons

PrimeBlocks


PrimeBlock是PrimeReact的付費版,他提供了一系列已經設計好的版型

其中有80種是可以免費體驗使用的,要在額外安裝套件npm install primeflex --save

React Development Tools

  • React Developer Tools (Chrome Extension)

React Developer Tools (Chrome Extension)

Extra: Mix DOM and React VirtualDOM

使用createElement來建立React element

const rootEl = document.createElement('div')
document.body.append(rootEl)

// 寫法一
const element = React.createElement('div', {
  children: 'Hello React!',
  className: 'container'
})

// 寫法二
const element = React.createElement(
'div', 
{ className: 'container' },
'Hello world'
)

ReactDOM.render(element, rootEl)

Custom useState


這部分會比較複雜,但能夠更進一步的了解useState實際的運作方式

React + Keycloak

Part 6.

react-keycloak


可以更方便的將尚未登入的user給導向keycloak登入頁面

要使用react-keycloak這個套件會需要安裝npm install @react-keycloak/web keycloak-js

首先要去設定要連線到的keycloak server的相關資訊

// keycloak.js
import Keycloak from 'keycloak-js'

const keycloak = new Keycloak({
    realm: "gatsby", // realm as configured in Keycloak
    url: "http://localhost:8080/auth/", // URL of the Keycloak server
    clientId: "gatsby-ui", // client id as configured in the realm in Keycloak
  })

export default keycloak

ReactKeycloakProvider

透過ReactKeycloakProvider可以讓React吃到keycloak-js中的連線設定

// App.js
import React from 'react'
import { ReactKeycloakProvider } from '@react-keycloak/web'
import keycloak from './keycloak'

const Loading = () => <div>Loading...</div>

export const wrapRootElement = ({ element }) => {
    
    return (
        <ReactKeycloakProvider 
            authClient={keycloak}
            initOptions={{
                onLoad: "login-required",
            }}
            LoadingComponent={<Loading />}
        >
            {element}
        </ReactKeycloakProvider>
    )
}

useKeycloak

useKeycloak這個hook可以讓我們方便的去取得登入狀態

// Home.js
import React from "react"
import { useKeycloak } from '@react-keycloak/web'

const Home = () => {
  const { keycloak, initialized } = useKeycloak()
  return (
      <>
        <div>The user is {keycloak.authenticated ? '' : 'NOT'} authenticated</div>
        {keycloak.authenticated && (
            <button type="button" onClick={() => keycloak.logout()}>
              Logout
            </button>
         )}
      </>
   )
}

export default Home

Call API with Keycloak

連線到有受到keycloak認證保護的backend server

// User.js
import React, {useEffect, useState} from 'react'
import axios from 'axios'
import {useKeycloak} from '@react-keycloak/web'

const Users = () => {
  const {keycloak, initialized} = useKeycloak()
  const [users, setUsers] = useState([])
  
  //load users when the component loads
  useEffect(() => {
    //call your backend api to load users
    axios.get("http://YOUR_BACKEND/users", {
      headers: {
        //put the keycloak access token in the Authorization header
        'Authorization': `Bearer ${keycloak.token}`
      }
    }).then((response) => {
      setUsers(response.data)
    })
  }, [])
  
  //render users...
}

Use Docker Compose to Deploy

Part 07.

Build image including frontend and backend


之後的檔案架構會類似底下

├─ backend/
├─ Dockerfile
├─ docker-compose.yml
└─ frontend/
    ├─ src/
    ├─ public/
    └─ build/

之前所寫的Dockerfile只有考慮到後端code的部分,這次會包含到build好的前端

RUN cd frontend && npm i && npm run build
RUN cd backend && npm i
WORKDIR /app/backend

不要用COPY * /app,不然會破壞掉原有的資料夾結構

Fastify Serving Frontend Update


由於fastify-static只能使用絕對路徑,因此我們要把code做一些小調整

讓不論在container內或是開發時都可以吃到正確的路徑

// server.js
const path = require('path')

fastify.register(require('@fastify/static'), {
    root: path.join(__dirname, '../frontend/build'),  // must use absolute path
})

再來就可以用docker build -t johnny12150/first_mern .來build image

Compose File


這裡會用docker-compose.yml來設定我們要一次啟動的container

由於keycloak會需要一些手動設定無法在container一跑起來就用,這裡會先忽略他

# docker-compose.yml
version: "3"

services:  # define containers
  frontend:  # container name
    image: johnny12150/first_mern
    ports:  # expose ports
      - "8000:8000"
    environment:
      # - MONGO_HOST=db
      - MONGO_HOST=host.docker.internal

Deploy


docker compose up -d可以同時啟動剛剛定義的兩個container

docker compose down -v可以刪掉剛剛的container並清除volume

# docker-compose.yml
# previous page...
  db:
    image: mongo:5.0.9
    ports:
      - "27018:27017"
    environment:  # env variables
      - MONGO_INITDB_ROOT_USERNAME=mongoadmin
      - MONGO_INITDB_ROOT_PASSWORD=secret

React URL not found

如果遇到原本react router用的url會找不到,就得去修改後端code

fastify.setNotFoundHandler(function (request, reply) {
  reply.sendFile('index.html')
})

這樣會讓所有endpoint找不到時都被重新導向前端

React SSR

React SSR + React Router

React-Router SSR原理

References