顯示具有 React 標籤的文章。 顯示所有文章
顯示具有 React 標籤的文章。 顯示所有文章

2020年12月31日 星期四

React 練習 - 使用 css, scss, img loader, image 壓縮

自己寫的 React, Webpack 練習,

練習了以下 webpack loader:


sass-loader : 

用來處理編譯 sass 檔案。


css-loader : 

用來處理 css 檔案。


style-loader : 

用來將 css 寫進網頁中 (不用在網頁中另寫 <link rel="stylesheet" href="xxx.css" />)。


image-webpack-loader : 

用來處理圖片檔案,有圖片壓縮等功能。


url-loader :

用來將圖片的 url 寫進網頁中,內部使用了 file-loader,所以可使用 file-loader 的選項參數,

有能把圖片的 url 轉成 base64 url 的等功能。


mini-css-extract-plugin : 

用來把處理好的 css 輸出成最後版本 css 檔案的功能,

網頁要使用 css 需自行加入 css style include, Ex:

<link rel="stylesheet" href="xxx.css" />)


實作了一個簡易的 tab 選單當範例來練習:

1. 有三個選單 tab,分別對應三個內容 (暫稱 content),內容中有各自的文字及各放置了不同的圖片。

2. 當滑鼠滑入 tab 中時  (mouseenter) ,下方會切換到對應的 Content。

檔案結構如下圖所示:


重要的程式碼如下 (以下根目錄以從 package.json 檔所在位置,也就是 WebContent 資料夾算起):

/index.html :
<!DOCTYPE html>
<html>
    <head>
        
    </head>
    <body>
        <div id="root"></div>
        <script src="dist/js/index_bundle.js"></script>
    </body>
</html>
/dev/js/index.jsx :
import React from "react"
import ReactDOM from "react-dom"
import {Main} from "./components/Main.jsx"

import "@projectDirPath/dev/css/style.scss"

ReactDOM.render(<Main/>, document.getElementById("root"));
/dev/js/components/Main.jsx :
import React from "react"
import {Tab} from "./Tab.jsx"
import {Content} from "./Content.jsx"
import dogImgSrc from "@projectDirPath/dev/img/dog.jpg"
import catImgSrc from "@projectDirPath/dev/img/cat.jpg"
import bananaCatImgSrc from "@projectDirPath/dev/img/bananaCat.jpg"

function Main(){
    const [itemList, setItemList] = React.useState([
                                                        {
                                                            title: "title1",
                                                            desc: "desc1",
                                                            img: dogImgSrc,
                                                            isActive: true
                                                        },
                                                        {
                                                            title: "title2",
                                                            desc: "desc2",
                                                            img: catImgSrc,
                                                            isActive: false
                                                        },
                                                        {
                                                            title: "title3",
                                                            desc: "desc3",
                                                            img: bananaCatImgSrc,
                                                            isActive: false
                                                        }
                                                    ]);

    function setItemActive(index){
        var updatedItemList = itemList.map((item, i) => {
            item.isActive = (i == index);
            return item;
        });

        setItemList(updatedItemList);
    }

    return (
        <React.Fragment>
            <div className="tab">
                {
                    itemList.map((item, index) => {
                        return <Tab item={item} key={index} index={index} setItemActive={setItemActive}/>;
                    })
                }
            </div>
            <div className="content">
                {
                    itemList.map((item, index) => {
                        return <Content item={item} key={index}/>;
                    })
                }
            </div>
        </React.Fragment>
    )
}

export {Main}
/dev/js/components/Content.jsx :
import React from "react"

function Content({item}){
    return (
        <div className={item.isActive ? "active" : ""}>{item.desc}<img src={item.img}/></div>
    );
}

export {Content}
/dev/js/components/Tab.jsx :
import React from "react"

function Tab({item, index, setItemActive}){

    function handleMouseEnter(){
        setItemActive(index);
    }

    return (
        <div className={item.isActive ? "active" : ""} onMouseEnter={handleMouseEnter}>{item.title}</div>
    );
}

export {Tab}
/dev/css/style.scss :
.tab {
    div {
        display: inline-block;
        width: 30%;
        height: 100px;
        background-color: green;
        background-color: yellow;
        border-bottom: 3px solid black;
    }

    div.active {
        background-color: yellow;
        border-left: 3px solid black;
        border-right: 3px solid black;
        border-top: 3px solid black;
        border-bottom: 3px solid yellow;
    }
}
  
.content {
    background-color: yellow;
    border-left: 3px solid black;
    border-right: 3px solid black;
    border-bottom: 3px solid black;

    div {
        display: none;
        padding: 3px;
    }

    div.active {
        display: block;
    }

    img {
        width : 30%;
    }
 }


webpack.config.js:

  var path = require('path');
//const MiniCssExtractPlugin = require('mini-css-extract-plugin');

module.exports = {
 entry : "./dev/js/index.jsx",
 output : {
  path : path.resolve(__dirname, "dist"),
  filename : "js/index_bundle.js",
  publicPath : "dist/"
 },
 resolve: {
    alias: {
        '@projectDirPath': __dirname
    }
},
 watch : false,
 devtool : 'source-map',
 mode : "development", //"production",
 module : {
  rules : [ {
   test : /\.jsx?$/,
   use : {
    loader : 'babel-loader',
    options : {
     presets : [ '@babel/preset-react', '@babel/preset-env' ]
    }
   }
  },
  {
    test: /\.(png|jpg|gif|jpe?g|svg)$/,
    use: [
      {
        loader: 'url-loader',
        options: {
          limit: 50 * 1024,
          outputPath: "img"
        }        
      },
      "image-webpack-loader"      
    ]
  },/*
  {
      test : /\.css$/,
      use : ["style-loader", "css-loader"]
  },*/
  {
    test: /\.s[ac]ss$/,
    use: [        
         "style-loader", //MiniCssExtractPlugin.loader,
         "css-loader", "sass-loader"
        ]
  }]
 }
 //, plugins: [new MiniCssExtractPlugin()]
}
  

package.json

  {
  "name": "reacttest",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "build": "webpack",
    "runServer": "lite-server"
  },
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@babel/core": "^7.7.5",
    "@babel/preset-env": "^7.7.5",
    "@babel/preset-react": "^7.7.4",
    "babel-loader": "^8.0.6",
    "css-loader": "^5.0.1",
    "file-loader": "^6.2.0",
    "image-webpack-loader": "^7.0.1",
    "lite-server": "^2.6.1",
    "mini-css-extract-plugin": "^1.3.3",
    "node-sass": "^5.0.0",
    "sass-loader": "^10.1.0",
    "style-loader": "^2.0.0",
    "url-loader": "^4.1.1",
    "webpack": "^4.41.2",
    "webpack-cli": "^3.3.10"
  },
  "dependencies": {
    "react": "^16.12.0",
    "react-dom": "^16.12.0",
    "react-redux": "^7.2.2",
    "redux": "^4.0.5"
  }
}

  

原始碼分享下載:

ReactTest2.7z


參考:

  1. Webpack 4 筆記 (3)
  2. 新手向 Webpack 完全攻略 (7) – 編譯圖片?略懂略懂
  3. Webpack 前端打包工具 - 使用 image-webpack-loader 壓縮圖片
  4. Webpack 前端打包工具 - 使用 sass-loader 編譯 Sass/SCSS 預處理器
  5. Webpack 前端打包工具 - 使用 mini-css-extract-plugin 把 CSS 抽離出來
  6. NPM – Webpack – 使用 import 的絕對路徑方法

2020年10月21日 星期三

React - Redux + Hook 練習 - ToDo List (待辦事項) 簡易實作

Hook 是 React v16.8 的新功能,讓我們在撰寫 React Component 時,

可以讓我們用 function 的寫法來取代 Class 的寫法,

來避免在 Class 中常會碰到的問題,例如:

1. Class 中的 this 意義常讓人無法容易地理解。

2. Component 上的事件 (event) 綁定,常須要多寫 bind() 的步驟,並且涵義也容易讓人不容易了解,例如:

class TextInput extends React.Component{

	constructor(props){

		super(props);

		this.onHandleTextChanged = this.onHandleTextChanged.bind(this);

	}

	onHandleTextChanged(e){

		this.props.handleTextChanged(e.target.value);

	}



	render(){

		return (

			<div>

					<input type="text" value={this.props.text} onChange={this.onHandleTextChanged} />

			</div>

		);

	}

}

而因為 function 不像 Class 可以儲存保存自己的成員變數,

為了可以實現像 Class 那樣保存、修改依賴於 Component 的成員變數,

React 使用了 useState() 這個 function,其可以以一個初始值做為輸入,

並返回一個包含 變數、修改變數的 function 的 array,

Ex:

import { useState } from "react"

const [text, setText] = useState("initial value");


React Redux 也推出了使用 React Hook 的版本,

其提供了 useSelector() 和 useDispatch() ,

useSelector() 可以讓我們方便得取得 Reducer Storde 裡 state 的值,

而不用像以前一樣使用

connect(mapStateToProps, actionCreators)

this.props.XXX

等的方法來取得 state 值,

Ex:

import {useSelector} from 'react-redux'

const toDoList = useSelector(state => state.toDoListReducer.toDoList);


useDispatch() 可以讓我們方便得送入要傳給 Reducer 的 Action 來觸發 Component render 元件繪製的行為,

而不用像以前一樣使用

connect(mapStateToProps, actionCreators)

this.props.xxxAction()

等的方法來傳遞 Action 給 Reducer,

Ex:

import {useDispatch} from 'react-redux'

import * as actionCreators from './action'

const dispatch = useDispatch();

function handleDeleteToDo(){

dispatch(actionCreators.deleteToDo(toDo.id));

}


這篇文要用 React Hook 來改寫上一篇,

React - Redux 練習 - ToDo List (待辦事項) 簡易實作

做為練習,並比較沒用 Hook 及有用 Hook 兩者程式碼的差異。


跟之前的例子比,有改動的檔案只有:

Main.jsx

TextDisplayer.jsx

TextInpupt.jsx

ToDoList.jsx

ToDoRow.jsx

如下圖所示


接著來看程式碼的部份:

Main.jsx :

import React, { useState } from "react"
import {TextDisplayer} from "./TextDisplayer.jsx"
import TextInput from "./TextInput.jsx"
import ToDoList from "./ToDoList.jsx"

function Main(){
	const [text, setText] = useState("");	

	return (
		<React.Fragment>
			<TextInput text={text} handleTextChanged={setText}/>
			<TextInput text={text} handleTextChanged={setText}/>
			<TextDisplayer text={text}/>
			<ToDoList/>
		</React.Fragment>
	);
}

export default Main

說明:

可以看到原本的 Main 不用 Class ,而改用了 function 去改寫,

因為只是 function ,所以連 constructor() 也不用了。

原本在用 Class Component 時,Component 所管理的 text 這個成員屬性,

是用 this.state = {text : ""} 的方式指定初始值的。

改用 Function Component 時,我們使用了 useState(初始值) 的方式來獲得設定了初始值的 text 變數,並且還得到了一個可以改變 text 的 function,即 setText() ,

const [text, setText] = useState("");

中的 text 及 setText() ,就跟以前一樣,

可以使用在 Main Component 中做UI的顯示,

也可以傳遞給 Child Component ,讓下層元件可以顯示及修改 text。

例如在 Main Component 中,我們把 text 及  setText() 傳遞給 TextInput、TextDisplayer 及 ToDoList。


TextDisplayer.jsx :

import React from "react"

function TextDisplayer({text}){
    return (<div>Text you typed : {text}</div>);
}

export {TextDisplayer}

說明:

TextDisplayer Component  也寫成了 Hook 的 Function Component 的形式,

此時接收上層元件的值改由從 Function Component 的輸入值傳進來,

輸入會是一個 Object, key 及 value 由上層元件傳進來的時候指定,

例如 在 Main.jsx 中用 text={text} 的方式傳 text 進 TextDisplayer 元件,

TextDisplayer 的 function component 輸入就會是:

{text : text的值} 


TextInpupt.jsx :

import React from "react"

import {useDispatch} from 'react-redux'
import * as actionCreators from './action'

function TextInput({text, handleTextChanged}){	
	const dispatch = useDispatch();

	function onHandleTextChanged(e){
		handleTextChanged(e.target.value);
	}

	function onHandleAddToDo(e){
		dispatch(actionCreators.addToDo(new Date().getTime(), text));
		handleTextChanged("");
		e.preventDefault();
	}

	return (
		<div>
			<form onSubmit={onHandleAddToDo}>
				<input type="text" value={text} onChange={onHandleTextChanged} />
				<input type="submit" value="addToDo"/>
			</form>
		</div>
	);
}

export default TextInput

說明:

TextInput Component 也被改寫成了 Hook 的 Function Component 形式,

此時不像 Class Component 時使用 this.props 的方式來取得上層元件傳進來的值,

而是直接從 Function Component 的 Input 輸入取得,

輸入會是一個 Object,其中會包括由上層元件傳進來的值,例如此例就是 Main.jsx 傳進來的

text 及 handleTextChanged 。

可以注意到的是, onChane 上面綁定的 onHandleTextChanged(),

不用再像以前 Class Component 那樣須要使用 bind() ,例如以前的寫法:

this.onHandleAddToDo = this.onHandleAddToDo.bind(this);

另一個可以注意到的是,我們沒有使用到如

export default connect(mapStateToProps, actionCreators)(TextInput)

的語法去得到 Reducer Store 裡的變數及函式,

那我們要怎麼把 Action 傳遞給 Reducer 呢?

答案是使用 Redux Hook 提供的 useDispatch() ,

其他傳回一個函式 dispatch(要送給 Reducer 的 Action) ,

我們可以直接把要傳遞的 Actoin 傳給 Reducer。


ToDoList.jsx :

import React from "react"
import ToDoRow from "./ToDoRow.jsx"

import {useSelector} from 'react-redux'

function ToDoList(){
	const toDoList = useSelector(state => state.toDoListReducer.toDoList);

	return (
		<ul>
			{	
				toDoList.map(function(toDo){
					return (
						<ToDoRow toDo={toDo} key={toDo.id}/>
					);
				})
			}
		</ul>
	);
}

export default ToDoList

說明:

ToDoList 元件跟 TextInput 元件一樣,沒有使用 connect() 來從 Reducer Store 取得資料,

那要怎麼取得 Reducer Store 中取得資料呢? 例如此例是要取得 toDoList。

答案是使用 Redux Hook 提供的 useSelector(Reducer 的 state) ,useSelector()會有 Redcuer 的 state 作為 callback 輸入,這樣我們就能取得 Reducer State 裡的資料,

在這裡因為我們之前有在 reducer.js 裡使用了 combineReducers() 去 combine toDoListReducer,所以取得 toDoList 的方式為 state.toDoListReducer.toDoList ,

如果沒有用 combineReducers() 的話,就會改成用

state.toDoList 直接取得值。


ToDoRow.jsx :

import React from "react"

import {useDispatch} from 'react-redux'
import * as actionCreators from './action'

function ToDoRow({toDo}){
	const dispatch = useDispatch();

	function handleDeleteToDo(){
		dispatch(actionCreators.deleteToDo(toDo.id));
	}

	return (
		<li>
			{toDo.toDoText}
			<button onClick={handleDeleteToDo}>Delete</button>
		</li>
	);
}

export default ToDoRow

說明:

ToDoRow 元件從本身 Function Component 的輸入中取得由 ToDoList 元件來的 toDo,

並且跟 TextInput 元件一樣使用了 dispatch() 來傳遞要送給 Reducer 的 Action,此例為 actionCreators.deleteToDo(toDo.id)

-------------------------------------------------------------------------------------

最後我們可以大概總結一下在這篇改用 React Hook 的文中,跟

React - Redux 練習 - ToDo List (待辦事項) 簡易實作

這篇沒使用 React Hook 文不一樣的地方:

  1. Component 元件全部由 Class 寫法改成 Function 寫法,
    故少了 Class 特性的語法,例如: constructor()、 this 的用法、 bind() 的用法。
  2. Component 自己管理的 state (例如 this.state = {text}),因為無法使用 Class 的 this 寫法,
    所以改成用 Hook 來管理,使用方法為 [變數名, 可修改變數的 function] = useState(初始值)。
  3. 上層把變數傳給下層的方法跟以前一樣,只是下層 Hook Component 因為不是 Class,
    所以不用 this.props ,改用從 Function Component 本身的輸入去取得,例如: 
    function ChildComponent({inputValue1, inputValue2, ...})
  4. 在取得 Reducer 管理的 state 方面,不使用 connect() 的方法來將 reducer state 及 action 綁到 this.props 上 ,而是使用 Redux Hook 所提供的 useSelector() 來取得,例如:
    var 要取得的 state value 變數 = useSelector(function(reducer的state){return 要的 value 變數});
  5. 在傳遞 Action 給 Reducer 方面,不使用 connect() 的方法來將 reducer state 及 action 綁到 this.props 上,而是使用 Redux Hook 所提供的 useDispatch() ,
    useDispatch() 可以回傳一個 dispatch(),
    dispatch() 可以輸入一個 action,其會將 action 傳遞給 Reducer,例如:
    var dispatch = useDispatch();
    dispatch(要傳遞的 action)。


最後要注意的地方是,

useState() 、useSelector()、useDispatch() 等

都要在 Function Component 的最上層呼叫,

不要在迴圈、條件式或是巢狀的 function 內呼叫 Hook (參考)。

要理解這個,我們可以從 Hook 實作的原理去理解。


首先是,為什麼元件寫成 function ,卻可以如 Class 搬擁有自己可管理的 state (例如 text),

因為 function 裡的所有變數應該都只是區域變數而己 (local variable),

應該無法長期保存其值 (不管有沒有被修改)。


React Hook 的實作方式可以簡單理解為 (**不代表真實實作的細節,只是類似,可以從這想法大致理解 Hook 的概念),

使用了兩個(或多個,端看 React Hook 的詳細實作)陣列 (或鍵結串列等類似有序串列的資料結構) 來儲存了所有使用

useState() 設定的 值 (暫且稱作 state value) 及 可修改值的function (暫且稱作 setState function),例如:

[value1, value2, value3, ...]  (暫且稱作 state array)

[setValu1, setValu2, setValu3, ...]  (暫且稱作 setState array)

在 React 要重新繪製 (Render) 所有元件時,就會依序執行所有function component 的 function,

如果是第一次繪製,呼叫 useState() 時,各 state value 會用初始值儲存在 state array 中,

並且在相應的 index 中存放可修改 value 的函式在 setState array 中 (所以函式會知道它要去修改哪個 state value)。

如果是之後的繪製,呼叫 useState() 時,各 state value 會以同樣順序的方式從 state array 中取出,

因為 state array 只是個陣列,所以每次取出時必須要照順序,因為它並不知道要取出的 state value 是給哪個 Component 用的,

這樣就能理解,為什麼在 Component 中,只能一次的,在最上層呼叫 useState(),

因為這樣才能確保取出 state value 時,每次都是照一樣的順序



源碼分享下載:
ReactReduxTest.7z

本Blog的其他相關文章:



參考資料:

  1. 使用 State Hook
  2. 打造你自己的 Hook
  3. React Redux | 小孩子才做選擇! Hooks 和 Redux 我全都要!
  4. 原始碼解析 React Hook 構建過程:沒有設計就是最好的設計
  5. React Hooks 不是黑魔法,只是陣列
  6. [译] 深入 React Hook 系统的原理
  7. Hooks
  8. How to pass a React Hook to a child component

2020年3月7日 星期六

React - Redux 練習 - ToDo List (待辦事項) 簡易實作

React 如果使用的元件嵌套過於多層,
需要用到的 state 傳值就會必須由上層元件一路傳到下層子元件,
當傳值層數過多的時候,會造成每一層的子元件都要撰寫傳值的程式碼,
而最下層如果要改變某個 state 值,要呼叫 root 元件的某 function 時,
又需要把 function 從 root 元件一路傳到子元件。

例如:
<Element1>
          <Element2 attr={this.state.attr} func1={this.func1}></Element2>
</Element1>

<Element2>
          <Element3 attr={this.props.attr} func1={this.props.func1}></Element3>
</Element2>

<Element3>
          <Element4 attr={this.props.attr} func1={this.props.func1}></Element4>
</Element3>

<Element4>...............</Element4>

而且一路傳值的中間,有的元件可能根本不需要用到某些值。

為了處理上述的問題,
有了 Redux 這樣的一個工具可以幫助我們,
Redux 並不完全是為 React 而開發的,
不過它的概念可以幫助我們。

我們希望可以把 React 的各層元件中,
所用到的 state, function 等都集中在最上層的某個地方來管理,
並且把 state, function 一路傳到 Root 元件上的各子元件中以供利用,
但又不想把傳值的程式碼寫在各子元件上 。

Redux 的想法跟我們很像,
它用 store 來管理 state,state 只會被叫做 action 的 function 來改變,
並且我們可以把 callback function 註冊  (subscribe) 給 store,
只要呼叫 action 來改變 state 時,被註冊的 callback function就會被執行。

對應 React 的概念是,
用 Redux 來管理 state,
用 action 來改變 state,
將 React Render 繪製元件的 function (相當於 render() ) 註冊到 store 上,
將 store 的 state, action 一路傳給所有子元件,
子元件可以從 store 中取得 state 來顯示,
也可取得 action 來改變 state,
而action 被執行時就會執行被註冊的 callback function 來繪製 React 元件。

不過通常我們不會直接使用 redux 在 react 中,
而是使用 react-redux,
react-redux  幫我們處理並優化了 react 與 redux 合作時的細節部份,
並且已經註冊了 react render 繪制 function,
我們只要專注使用 action 來改變 state,
react-redux 就會幫我們自動重繪元件

在這篇文中,我修改了
這篇
React 練習 - ToDo List (待辦事項) 簡易實作
,將它用 Redux + React 的方式改寫

首先安裝 redux 和 react-redux
npm install redux
npm install react-redux



使用 Redux 管理一個統一的 state,其可在各 component 內之間使用,
state 中包括一般 state 及可跟 reducer 配合的 actionCreator

reducer :
Redux 用來決來決定其管理 state 的依據,
為一個 function,輸入為"前一個 state" 和 "要做的行為 action (通常為一個物件,內含純文字的 type 和 action 需要的參數)",返回值為更新後的 state:
Ex :
newState reducer(oldState, action){
     var newState = defaultState;
     switch (action.type){
          case "action1" :
              newState  = .........;
          break;
     }
     return newState;
}

actionCreator :
為多個 function 的集合,共通點是返回 reducer function 所需的 action 物件,
actionCreator 中的各 function 會傳遞給各 React 子元件中以供使用
Ex:
returnAction addToDo(id, text){
     return {id : id, text : text};
}

store :
Redux 所管理的 state

跟之前沒有使用 redux 的範例比較,
我們增加了 action.js 及 reducer.js


/src/components/main/reducer.js :
import { combineReducers } from 'redux'

function toDoListReducer(state = {toDoList: []}, action) {
  /*
  state : {
 toDoList : [{id : new Date().getTime(),toDoText : ""}]]
  }
  */
  switch (action.type) {
    case 'ADD_TODO':
      {
  //action.payload : [{id : new Date().getTime(),toDoText : ""}]  
  if (action.payload.id && action.payload.toDoText.trim()){
   return {
    toDoList : state.toDoList.concat([action.payload])
   };
  }else{
   return state;
  }
      }

    case 'DEL_TODO':
      {
  //action.payload : {id : toDoId}
  return {
   toDoList : state.toDoList.filter(function(toDo){
    return toDo.id != action.payload.id;
   })
  }
      }

    default:
      return state
  }
}

const reducers = combineReducers({
  toDoListReducer
})

export default reducers

說明:
在 reducer.js 中,宣告了一個 toDoListReducer() 來處理 state 及 action 的變化,
根據輸入的 actions 和上一次的 state ,回傳不同的新 state,
state 及 action 的物件格式可以由自己決定,
在這裡, state 裡管理一個 toDoList 陣例,其中存放各個待辦事項的 id 及文字內容。
而 action 裡有 type 及 payload 兩個屬性,type 用來表示是哪種 action,
而 payload 存放此 action 所需的其他資訊。

可以注意到的是, reducer function 可以不只一個 (雖然本例只有一個),
當有多個 reducer function 時,redux 提供了 combinReducers 來將所有的 reducer function
包起來一起 export 出去。

/src/components/main/action.js :
export const addToDo = function(id, toDoText){
 //action.payload : [{id : new Date().getTime(),toDoText : ""}]  
 return { 
  type: 'ADD_TODO',
  payload : {
   id : id,
   toDoText : toDoText
  }
 };
}

export const deleteToDo = function(id){
 //action.payload : {id : toDoId}
 return { 
  type: 'DEL_TODO',
  payload : {
   id : id
  }
 };
}

說明:
在 action.js  中,宣告了想要改變 toDoList 值的各種行為 function,
這些 function 之後會用 redux 傳進各元件中以供使用,
當各元件呼叫這些 action function 時, action function 會將其轉成 reducer function 需要的格式,例如此例中的
{
      type: "someAction",
      payload : { /* some infomation */}
}

/src/index.jsx :
import React from "react"
import ReactDOM from "react-dom"
import Main from "./components/main/Main.jsx"

import {Provider} from 'react-redux'
import {createStore} from 'redux'
import reducers from './components/main/reducer'

//below code if used for Chrome plugin, REDUX_DEVTOOLS
//const store = createStore(reducers, window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__());
const store = createStore(reducers);

ReactDOM.render(
 <Provider store={store}>
  <Main/>
 </Provider>, 
 document.getElementById('root')
);

說明:
接下來,在 index.jsx 中,即在最頂層的 root element 中,
我們先利用 react 提供的 createStore() 結合之前建立的 reducer 來得到交結 redux 管理的 store,
再利用了 react-redux 提供的 <Provider> 元件,
來將 store 傳給所有子元件。
需要注意的是,我們只是將 store 傳給子元件,只包含了 state 及 action 的關係。
之後再各子元件中,可依各自的需要將 store 的 state 及 action 取出來使用。
也就是不用再像沒有 redux 時,一定要用 this.props 從父元件獲取,
父元件也不用特別寫傳值的程式碼。

題外:
REDUX_DEVTOOLS 是一個 Chrome 的 Redux 外掛,安裝後,在使用 conntect() 時用以下的方式:
const store = createStore(reducers, window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__());
即可以在 Chrome 中對 Redux 進行 debug,例如觀察 state 的變化。
REDUX_DEVTOOLS 應用程式商店
REDUX_DEVTOOLS 官網

我們這次只有利用 redux 來管理待辦事項,
沒有委託 redux 來管理 User 在輸入框中即時輸入的字串,
所以 TextDisplayer.jsx 完全不需要修改,還是使用 react 本來管理的方式 。

/src/components/main/Main.jsx:
import React from "react"
import {TextDisplayer} from "./TextDisplayer.jsx"
import TextInput from "./TextInput.jsx"
import ToDoList from "./ToDoList.jsx"

class Main extends React.Component{
	constructor(props){
		super(props);
		
		this.state = {
			text : ""
		};
		
		this.handleTextChanged = this.handleTextChanged.bind(this);
	}
	
	handleTextChanged(text){
		this.setState({text : text});
	}
	
    render(){
        return (
        	
        		
        		
        		
				
        	
    	);
    }
}

export default Main


/src/components/main/TextInput.jsx :
import React from "react"

import {connect} from 'react-redux'
import * as actionCreators from './action'

class TextInput extends React.Component{
 constructor(props){
  super(props);
  
  this.onHandleTextChanged = this.onHandleTextChanged.bind(this);
  this.onHandleAddToDo = this.onHandleAddToDo.bind(this);
 }
 
 onHandleTextChanged(e){
  this.props.handleTextChanged(e.target.value);
 }

 onHandleAddToDo(e){
  // addToDo() is set to this.props by connect(mapStateToProps, actionCreators)(TextInput)
  this.props.addToDo(new Date().getTime(), this.props.text);
  this.props.handleTextChanged("");
  e.preventDefault();
 }

 render(){
  return (
   <div>
    <form onSubmit={this.onHandleAddToDo}>
     <input type="text" value={this.props.text} onChange={this.onHandleTextChanged} />
     <input type="submit" value="addToDo"/>
    </form>
   </div>
  );
 }
}

const mapStateToProps = function(store){
 return { 
  storeState: store.toDoListReducer
 };
}
export default connect(mapStateToProps, actionCreators)(TextInput)

在 TextInput.jsx 中,我們需要取得 addToDo() 來添加待辦事項到 toDoList 裡。
我們使用了 react-redux 的 connect(mapStateToProps, actionCreators)(someElement)
來將 redux 管理的 state 、 action 並結合  store 中的 reducer 一起取出來放到 TextInput 元件的 props上。
在 TextInput 元件中,就可以使用 this.props.addToDo() 來增加待辦事項。
(為了方便,我們在 TextInput 中取出了所有的 state 及 action,但其實可以只取出部份的 state 及 action,例如 deleteToDo() 其實可以不用取出,因為沒有用到 )

接下來其他的元件: ToDoList.jsx、ToDoRow.jsx
也是用一樣的方式取出所需的 state, action,
用 this.props.someStateOrAction 來使用。
以下為程式嗎:

/src/components/main/ToDoList.jsx :
import React from "react"
import ToDoRow from "./ToDoRow.jsx"

import {connect} from 'react-redux'
import * as actionCreators from './action'

class ToDoList extends React.Component{ 
 render(){
  return (
   <ul>
    {  
     // toDoList is set to this.props.storeState by connect(mapStateToProps, actionCreators)(ToDoList)
     this.props.storeState.toDoList.map(function(toDo){
      return (
       <ToDoRow toDo={toDo} key={toDo.id}/>
      );
     })
    }
   </ul>
  );
 }
}

const mapStateToProps = function(store){
 return { 
  storeState: store.toDoListReducer
 };
}
export default connect(mapStateToProps, actionCreators)(ToDoList)

/src/components/main/ToDoRow.jsx :
import React from "react"

import {connect} from 'react-redux'
import * as actionCreators from './action'

class ToDoRow extends React.Component{
 constructor(props){
  super(props);

  this.handleDeleteToDo = this.handleDeleteToDo.bind(this);
 }

 handleDeleteToDo(){
  //deleteToDo() is set to this.props by connect(mapStateToProps, actionCreators)(ToDoRow)
  this.props.deleteToDo(this.props.toDo.id);
 }

 render(){
  return (
   <li>
    {this.props.toDo.toDoText}
    <button onClick={this.handleDeleteToDo}>Delete</button>
   </li>
  );
 }
}

const mapStateToProps = function(store){
 return { 
  storeState: store.toDoListReducer
 };
}
export default connect(mapStateToProps, actionCreators)(ToDoRow)

源碼下載

2019年12月7日 星期六

React 練習 - ToDo List (待辦事項) 簡易實作

這篇要來記錄一下 React 的
ToDo List (待辦事項) 簡易實作(無用到 Redux 之類的)

首先先來看一下成果的樣子:

並將它分

接下來是需求說明:
  1. 有一個供使用者輸入的<input type="text"/> 框,使用者輸入待辦事項的文字後,
    按下Enter鍵(配合 <form>)或按下 "addToDo" 按鈕,可以加進下方的待辦事項顯示區域 (<ul><li>)。
    為了演示一下雙向資料流的實作,放了兩個待辦事項輸入框,
    在使用者在其中一個輸入框輸入資料時,另一個的輸入框會同步顯示輸入的待辦事項的內容。
  2. 使用者在輸入待辦事項的文字時,下方有一行文字可以顯示使用者正在輸入的文字。
  3. 下方的待辦事項清單中,各個待辦事項右方有一個 "Delete" 按鈕,按下可將待辦事項移除

再來把其需求分解成各個Component,如下圖所示:
  1. Main.jsx :
    統整所有 Component 的最上層 Component,也管理著 state。
  2. TextInput.jsx :
    供使用者輸入待辦事項的地方。
  3. TextDisplayer.jsx :
    顯示使用者正在輸入的待辦事項。
  4. ToDoList.jsx :
    顯示待辦事項例表的 Component,其內部用到了子 Component, "ToDoRow.jsx"。
  5. ToDoRow.jsx :
    顯示各個獨立待辦事項的 Component,包含著 Delete 等功能。