# Redux + React 環境建置
###### tags: `redux` `react.js` `javaScript` `font-end`
> [time= 2019 11 08 ]
> 原文 & 參考:
> https://chentsulin.github.io/redux/docs/basics/UsageWithReact.html
<br>
使用 Mac 建置環境:
找一個你想建立此專案的位置
此範例目錄
`~/你的本機路徑/redux-base/`
## 建置環境
*終端機目前位置`~/你的本機路徑/redux-base/`*
npm版本:
```=
$ npm -- version
```
>*6.9.0*
<br><br><br>
初始化環境:
```=
$ npm init -y
```
<br><br><br>
安裝 webpack 和 webpack cli:
```=
$ npm i webpack webpack-cli --save-dev
```
>*webpack@4.41.2*
>*webpack-cli@3.3.10*
<br><br><br>
現在開啟 `package.json` 配置 scripts:
將
```=
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
}
```
改成
```javascript=
"scripts": {
"dev": "webpack --mode development ./project/frontend/src/index.js --output ./project/frontend/static/frontend/main.js",
"build": "webpack --mode production ./project/frontend/src/index.js --output ./project/frontend/static/frontend/main.js"
}
```
<br><br><br>
安裝babel來編譯我們的代碼:
```=
$ npm i @babel/core babel-loader @babel/preset-env @babel/preset-react babel-plugin-transform-class-properties --save-dev
```
>*babel-loader@8.0.6*
>*@babel/preset-react@7.6.3*
>*@babel/core@7.6.4*
>*@babel/preset-env@7.6.3*
>*babel-plugin-transform-class-properties@6.24.1*
<br><br><br>
安裝 React 和 prop-types:
```=
$ npm i react react-dom prop-types --save-dev
```
>*prop-types@15.7.2*
>*react@16.11.0*
>*react-dom@16.11.0*
<br><br><br>
配置Babel,新增一個檔案命名為`.babelrc` 在裡面寫入:
```bable=
{
"presets": [
"@babel/preset-env", "@babel/preset-react"
],
"plugins": [
"transform-class-properties"
]
}
```
<br><br><br>
配置babel-loader,新增一個檔案命名為`webpack.config.js` 在裡面寫入:
```javascript=
module.exports = {
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: "babel-loader"
}
}
]
}
};
```
<br><br><br>
安裝 rudux
穩定版本
```
npm install --save redux
```
> *redux@4.0.4*
<br>
大多數情況,你也會需要 React 的綁定和開發者工具
```
npm install --save react-redux
npm install --save-dev redux-devtools
```
<br><br><br>
都安裝完成後目錄結構會是這樣
```
redux-base
├──node_modules
├──.babelrc
├──package.json
├──package-lock.json
└──webpack.config.js
```
<br><br><br>
## 開始選寫基礎的 Redux
*終端機目前位置`~/你的本機路徑/redux-base/`*
先建立選寫程式的目錄結構
```
$ mkdir -p ./project/frontend/static/frontend
$ mkdir -p ./project/frontend/src/
```
```
redux-base
├──node_modules
├──project
│ └──fontend
│ ├──src
│ └──static
│ └──frontend
│
├──.babelrc
├──package.json
├──package-lock.json
└──webpack.config.js
```
<br>
新增一個檔案命名為 `./project/frontend/index.html`
```htmlmixed=
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.6.2/css/bulma.min.css">
<title>Redux</title>
</head>
<body>
<section class="section">
<div class="container">
<div id="root" class="columns">
<!-- React -->
</div>
</div>
</section>
</body>
<script src="./static/frontend/main.js"></script>
</html>
```
`main.js` 在建置 webpack 時才會產生
<br><br><br>
### Actions
新增一個檔案命名為 `./project/frontend/src/actions.js`
```javascript=
/*
* action type
*/
export const ADD_TODO = 'ADD_TODO'
export const TOGGLE_TODO = 'TOGGLE_TODO'
export const SET_VISIBILITY_FILTER = 'SET_VISIBILITY_FILTER'
/*
* 其他常數
*/
export const VisibilityFilters = {
SHOW_ALL: 'SHOW_ALL',
SHOW_COMPLETED: 'SHOW_COMPLETED',
SHOW_ACTIVE: 'SHOW_ACTIVE'
}
/*
* action creator
*/
export function addTodo(text) {
return { type: ADD_TODO, text }
}
export function toggleTodo(index) {
return { type: TOGGLE_TODO, index }
}
export function setVisibilityFilter(filter) {
return { type: SET_VISIBILITY_FILTER, filter }
}
```
<br><br><br>
### Reducers
新增一個檔案命名為 `./project/frontend/src/reducers.js`
```javascript=
import { combineReducers } from 'redux'
import { ADD_TODO, TOGGLE_TODO, SET_VISIBILITY_FILTER, VisibilityFilters } from './actions'
const { SHOW_ALL } = VisibilityFilters
function visibilityFilter(state = SHOW_ALL, action) {
switch (action.type) {
case SET_VISIBILITY_FILTER:
return action.filter
default:
return state
}
}
function todos(state = [], action) {
switch (action.type) {
case ADD_TODO:
return [
...state,
{
text: action.text,
completed: false
}
]
case TOGGLE_TODO:
return state.map((todo, index) => {
if (index === action.index) {
return Object.assign({}, todo, {
completed: !todo.completed
})
}
return todo
})
default:
return state
}
}
const todoApp = combineReducers({
visibilityFilter,
todos
})
export default todoApp
```
<br><br><br>
### Store
新增一個檔案命名為 `./project/frontend/src/App.js`
```javascript=
import { createStore } from 'redux'
import todoApp from './reducers'
import { addTodo, toggleTodo, setVisibilityFilter, VisibilityFilters } from './actions'
let store = createStore(todoApp)
// 記錄初始 state
console.log(store.getState())
// 每次 state 變更,就記錄它
// 記得 subscribe() 會回傳一個用來撤銷 listener 的 function
let unsubscribe = store.subscribe(() =>
console.log(store.getState())
)
// Dispatch 一些 action
store.dispatch(addTodo('Learn about actions'))
store.dispatch(addTodo('Learn about reducers'))
store.dispatch(addTodo('Learn about store'))
store.dispatch(toggleTodo(0))
store.dispatch(toggleTodo(1))
store.dispatch(setVisibilityFilter(VisibilityFilters.SHOW_COMPLETED))
// 停止監聽 state 的更新
unsubscribe()
```
<br><br><br>
webpack 入口
新增一個檔案命名為 `./project/frontend/src/index.js`
```javascript=
import App from "./App";
```
<br><br><br>
程式都寫好後,下指令
```
$ npm run dev
```
執行完會在 `./frontend/static/frontend/` 新增一個 `main.js`
目錄結構
```
redux-base
├──node_modules
├──project
│ └──frontend
│ ├──src
│ │ ├──actions.js
│ │ ├──App.js
│ │ ├──reducers.js
│ │ └──index.js
│ │
│ ├──static
│ │ └──frontend
│ │ └──main.js
│ │
│ └──index.html
│
├──.babelrc
├──package.json
├──package-lock.json
└──webpack.config.js
```
<br><br><br>
都沒問題後,用 Chrome 開啟 `index.html`,到 Consol
就可以看到 `Store` 所執行的結果

<br><br><br>
## 搭配 React 運用
把 `./project/frontend/src/` 目錄裡的檔案都**刪儲**
*(你要另開資料夾備份起來也可以)*
建立選寫程式的目錄結構
```
$ mkdir -p ./project/frontend/src/components
$ mkdir -p ./project/frontend/src/containers
$ mkdir -p ./project/frontend/src/actions
$ mkdir -p ./project/frontend/src/reducers
```
```
redux-base
├──node_modules
├──project
│ └──frontend
│ ├──src
│ │ ├──actions
│ │ ├──components
│ │ ├──containers
│ │ └──reducers
│ │
│ ├──static
│ │ └──frontend
│ │ └──main.js
│ │
│ └──index.html
│
├──.babelrc
├──package.json
├──package-lock.json
└──webpack.config.js
```
<br><br><br>
### 進入點
新增 `./project/frontend/src/index.js`
```javascript=
import React from 'react'
import { render } from 'react-dom'
import { Provider } from 'react-redux'
import { createStore } from 'redux'
import todoApp from './reducers'
import App from './components/App'
let store = createStore(todoApp)
render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)
```
<br><br><br>
### Action Creator
新增 `./project/frontend/src/actions/index.js`
```javascript=
let nextTodoId = 0
export const addTodo = (text) => {
return {
type: 'ADD_TODO',
id: nextTodoId++,
text
}
}
export const setVisibilityFilter = (filter) => {
return {
type: 'SET_VISIBILITY_FILTER',
filter
}
}
export const toggleTodo = (id) => {
return {
type: 'TOGGLE_TODO',
id
}
}
```
### Reducer
新增 `reducers/todos.js`
```javascript=
const todo = (state = {}, action) => {
switch (action.type) {
case 'ADD_TODO':
return {
id: action.id,
text: action.text,
completed: false
}
case 'TOGGLE_TODO':
if (state.id !== action.id) {
return state
}
return Object.assign({}, state, {
completed: !state.completed
})
default:
return state
}
}
const todos = (state = [], action) => {
switch (action.type) {
case 'ADD_TODO':
return [
...state,
todo(undefined, action)
]
case 'TOGGLE_TODO':
return state.map(t =>
todo(t, action)
)
default:
return state
}
}
export default todos
```
<br><br><br>
新增 `reducers/visibilityFilter.js`
```javascript=
const visibilityFilter = (state = 'SHOW_ALL', action) => {
switch (action.type) {
case 'SET_VISIBILITY_FILTER':
return action.filter
default:
return state
}
}
export default visibilityFilter
```
<br><br><br>
新增 `reducers/index.js`
```javascript=
import { combineReducers } from 'redux'
import todos from './todos'
import visibilityFilter from './visibilityFilter'
const todoApp = combineReducers({
todos,
visibilityFilter
})
export default todoApp
```
<br><br><br>
### Presentational Component
新增 `components/Todo.js`
```javascript=
import React, { PropTypes } from 'react'
const Todo = ({ onClick, completed, text }) => (
<li
onClick={onClick}
style={{
textDecoration: completed ? 'line-through' : 'none'
}}
>
{text}
</li>
)
Todo.propTypes = {
onClick: PropTypes.func.isRequired,
completed: PropTypes.bool.isRequired,
text: PropTypes.string.isRequired
}
export default Todo
```
<br><br><br>
新增 `components/TodoList.js`
```javascript=
import React, { PropTypes } from 'react'
import Todo from './Todo'
const TodoList = ({ todos, onTodoClick }) => (
<ul>
{todos.map(todo =>
<Todo
key={todo.id}
{...todo}
onClick={() => onTodoClick(todo.id)}
/>
)}
</ul>
)
TodoList.propTypes = {
todos: PropTypes.arrayOf(PropTypes.shape({
id: PropTypes.number.isRequired,
completed: PropTypes.bool.isRequired,
text: PropTypes.string.isRequired
}).isRequired).isRequired,
onTodoClick: PropTypes.func.isRequired
}
export default TodoList
```
<br><br><br>
新增 `components/Link.js`
```javascript=
import React, { PropTypes } from 'react'
const Link = ({ active, children, onClick }) => {
if (active) {
return <span>{children}</span>
}
return (
<a href="#"
onClick={e => {
e.preventDefault()
onClick()
}}
>
{children}
</a>
)
}
Link.propTypes = {
active: PropTypes.bool.isRequired,
children: PropTypes.node.isRequired,
onClick: PropTypes.func.isRequired
}
export default Link
```
<br><br><br>
新增 `components/Footer.js`
```javascript=
import React from 'react'
import FilterLink from '../containers/FilterLink'
const Footer = () => (
<p>
Show:
{" "}
<FilterLink filter="SHOW_ALL">
All
</FilterLink>
{", "}
<FilterLink filter="SHOW_ACTIVE">
Active
</FilterLink>
{", "}
<FilterLink filter="SHOW_COMPLETED">
Completed
</FilterLink>
</p>
)
export default Footer
```
<br><br><br>
新增 `components/App.js`
```javascript=
import React from 'react'
import Footer from './Footer'
import AddTodo from '../containers/AddTodo'
import VisibleTodoList from '../containers/VisibleTodoList'
const App = () => (
<div>
<AddTodo />
<VisibleTodoList />
<Footer />
</div>
)
export default App
```
<br><br><br>
### Container Component
新增 `containers/VisibleTodoList.js`
```javascript=
import { connect } from 'react-redux'
import { toggleTodo } from '../actions'
import TodoList from '../components/TodoList'
const getVisibleTodos = (todos, filter) => {
switch (filter) {
case 'SHOW_ALL':
return todos
case 'SHOW_COMPLETED':
return todos.filter(t => t.completed)
case 'SHOW_ACTIVE':
return todos.filter(t => !t.completed)
}
}
const mapStateToProps = (state) => {
return {
todos: getVisibleTodos(state.todos, state.visibilityFilter)
}
}
const mapDispatchToProps = (dispatch) => {
return {
onTodoClick: (id) => {
dispatch(toggleTodo(id))
}
}
}
const VisibleTodoList = connect(
mapStateToProps,
mapDispatchToProps
)(TodoList)
export default VisibleTodoList
```
<br><br><br>
新增 `containers/FilterLink.js`
```javascript=
import { connect } from 'react-redux'
import { setVisibilityFilter } from '../actions'
import Link from '../components/Link'
const mapStateToProps = (state, ownProps) => {
return {
active: ownProps.filter === state.visibilityFilter
}
}
const mapDispatchToProps = (dispatch, ownProps) => {
return {
onClick: () => {
dispatch(setVisibilityFilter(ownProps.filter))
}
}
}
const FilterLink = connect(
mapStateToProps,
mapDispatchToProps
)(Link)
export default FilterLink
```
<br><br><br>
### 其它 Component
新增 `containers/AddTodo.js`
```javascript=
import React from 'react'
import { connect } from 'react-redux'
import { addTodo } from '../actions'
let AddTodo = ({ dispatch }) => {
let input
return (
<div>
<form onSubmit={e => {
e.preventDefault()
if (!input.value.trim()) {
return
}
dispatch(addTodo(input.value))
input.value = ''
}}>
<input ref={node => {
input = node
}} />
<button type="submit">
Add Todo
</button>
</form>
</div>
)
}
AddTodo = connect()(AddTodo)
export default AddTodo
```
程式都寫好後,下指令
```
$ npm run dev
```
<br>
目錄結構會如下
```
redux-base
├──node_modules
├──project
│ └──frontend
│ ├──src
│ │ ├──actions
│ │ │ └──index.js
│ │ │
│ │ ├──components
│ │ │ ├──App.js
│ │ │ ├──Footer.js
│ │ │ ├──Link.js
│ │ │ ├──Todo.js
│ │ │ └──TodoList
│ │ │
│ │ ├──containers
│ │ │ ├──AddTodo.js
│ │ │ ├──FilterLink.js
│ │ │ └──VisibleTodoList
│ │ │
│ │ ├──reducers
│ │ │ ├──index.js
│ │ │ ├──todos.js
│ │ │ └──visibilityFilter
│ │ │
│ │ └──index.js
│ │
│ ├──static
│ │ └──frontend
│ │ └──main.js
│ │
│ └──index.html
│
├──.babelrc
├──package.json
├──package-lock.json
└──webpack.config.js
```
都沒問題後,一樣用 Chrome 開啟 index.html,就可以看到簡單地的 Todo 應用了
<br><br><br>