React
實(shí)際上只是 UI
框架,通過 JSX
生成動(dòng)態(tài) dom 渲染 UI母赵,沒有架構(gòu)明垢、沒有模板、沒有設(shè)計(jì)模式市咽、沒有路由痊银、也沒有數(shù)據(jù)管理。所以需要借助其他工具施绎。
redux
npm install redux --save
什么是 redux ?
Redux
是JavaScript
狀態(tài)容器溯革,提供可預(yù)測(cè)化的狀態(tài)管理」茸恚可以理解為全局?jǐn)?shù)據(jù)狀態(tài)管理工具致稀,用來做組件通信等。為什么使用 redux ?
當(dāng)沒有使用redux
時(shí)兄弟組件間傳值將很麻煩俱尼,代碼很復(fù)雜冗余抖单。使用redux
定義全局單一的數(shù)據(jù)Store
,可以自定義Store
里面存放哪些數(shù)據(jù)遇八,整個(gè)數(shù)據(jù)結(jié)構(gòu)也是自己清楚的矛绘。-
redux 工作流 ?
redux 工作流- store:推送數(shù)據(jù)的倉(cāng)庫
- reducer:幫助 store 處理數(shù)據(jù)的方法(初始化、修改刃永、刪除)
- actions:數(shù)據(jù)更新的指令
- react 組件(UI):訂閱 store 中的數(shù)據(jù)
redux 用法:
import { createStore } from 'redux'
/*
* 這是一個(gè) reducer货矮,形式為 (state, action) => state 的純函數(shù)。描述了 action 如何把 state 轉(zhuǎn)變成下一個(gè) state斯够。
* state 的形式取決于你囚玫,可以是基本類型、數(shù)組读规、對(duì)象抓督、甚至是 Immutable.js 生成的數(shù)據(jù)結(jié)構(gòu)。
* 當(dāng) state 變化時(shí)需要返回全新的對(duì)象束亏,而不是修改傳入的參數(shù)铃在。
*/
function counter(state = 0, action) {
switch (action.type) {
case 'INCREMENT':
return state + 1
case 'DECREMENT':
return state - 1
default:
return state;
}
}
// 創(chuàng)建 Redux store 來存放應(yīng)用的狀態(tài)
// API 是 { subscribe, dispatch, getState }
const store = createStore(counter);
// 可以手動(dòng)訂閱更新,也可以事件綁定到視圖層枪汪。
store.subscribe(() =>
const sotreState = store.getState()
......
)
// 改變內(nèi)部 state 惟一方法是 dispatch 一個(gè) action涌穆。
store.dispatch({ type: 'INCREMENT' })
store.dispatch({ type: 'DECREMENT' })
-
redux 三大原則:
單一數(shù)據(jù)源:整個(gè)應(yīng)用的 state 存放在唯一的一個(gè) store 中怔昨。
store.getState()
state 是只讀的,唯一改變 state 的方法就是觸發(fā) action宿稀,action 是一個(gè)用于描述已發(fā)生事件的普通對(duì)象趁舀。
store.dispatch({ type: 'COMPLETE_TODO', index: 1 })
- 使用純函數(shù)來執(zhí)行修改(reducer:接收先前的 state 和 action,并返回新的 state)
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 'COMPLETE_TODO': return state.map((todo, index) => { if (index === action.index) { return Object.assign({}, todo, { completed: true }) } return todo }) default: return state } } import { combineReducers, createStore } from 'redux' const reducer = combineReducers({ visibilityFilter, todos }) const store = createStore(reducer)
React-Redux
npm install react-redux --save
React-Redux
是 Redux
的官方 React
綁定庫祝沸。它能夠使你的 React
組件從 Redux store
中讀取數(shù)據(jù)矮烹,并且向 store
分發(fā) actions
以更新數(shù)據(jù)
-
React-Redux
將所有組件分成兩大類:UI
組件和容器組件。UI
組件負(fù)責(zé)UI
的呈現(xiàn)罩锐,容器組件負(fù)責(zé)管理數(shù)據(jù)和邏輯奉狈。-
UI
組件:只負(fù)責(zé) UI 的呈現(xiàn),不帶有任何業(yè)務(wù)邏輯涩惑;沒有狀態(tài)(即不使用this.state
這個(gè)變量)仁期;所有數(shù)據(jù)都由參數(shù)this.props
提供;不使用任何Redux
的API
- 容器組件:負(fù)責(zé)管理數(shù)據(jù)和業(yè)務(wù)邏輯竭恬,不負(fù)責(zé)
UI
的呈現(xiàn)跛蛋;帶有內(nèi)部狀態(tài);使用Redux
的API
痊硕。
-
React-Redux
規(guī)定赊级,所有的UI
組件都由用戶提供,容器組件則是由React-Redux
自動(dòng)生成岔绸。也就是說理逊,用戶負(fù)責(zé)視覺層,狀態(tài)管理則是全部交給它盒揉。connect()
import { connect } from 'react-redux'
const VisibleTodoList = connect(mapStateToProps, mapDispatchToProps)(TodoList)
上面 VisibleTodoList
便是 UI
組件 TodoList
通過 connect
方法自動(dòng)生成的容器組件晋被。
connect
方法接受兩個(gè)參數(shù):mapStateToProps
和 mapDispatchToProps
。它們定義了 UI
組件的業(yè)務(wù)邏輯预烙。前者負(fù)責(zé)輸入邏輯墨微,即將 state
映射到 UI
組件的參數(shù) props
,后者負(fù)責(zé)輸出邏輯扁掸,即將用戶對(duì) UI
組件的操作映射成 Action
。
- mapStateToProps()
const mapStateToProps = (state) => {
return {
todos: getVisibleTodos(state.todos, state.visibilityFilter)
}
}
mapStateToProps
是一個(gè)函數(shù)最域,它接受 state
作為參數(shù)谴分,返回一個(gè)對(duì)象。這個(gè)對(duì)象有一個(gè) todos
屬性镀脂,代表 UI
組件的同名參數(shù)牺蹄,后面的 getVisibleTodos
也是一個(gè)函數(shù),可以從 state
算出 todos
的值薄翅。
mapStateToProps
建立一個(gè)從(外部的)state
對(duì)象到(UI
組件的)props
對(duì)象的映射關(guān)系沙兰。執(zhí)行后應(yīng)該返回一個(gè)對(duì)象氓奈,里面的每一個(gè)鍵值對(duì)就是一個(gè)映射。
- mapDispatchToProps()
mapDispatchToProps
用來建立UI
組件的參數(shù)到store.dispatch
方法的映射鼎天。它定義了哪些用戶的操作應(yīng)該當(dāng)作Action
舀奶,傳給Store
。它可以是一個(gè)函數(shù)斋射,也可以是一個(gè)對(duì)象育勺。
- 是函數(shù)則會(huì)得到
dispatch
和ownProps
(容器組件的props
對(duì)象)兩個(gè)參數(shù)。
const mapDispatchToProps = (dispatch, ownProps) => {
return {
onClick: () => {
dispatch({
type: 'SET_VISIBILITY_FILTER',
filter: ownProps.filter,
})
}
}
}
- 是一個(gè)對(duì)象罗岖,它的每個(gè)鍵名也是對(duì)應(yīng)
UI
組件的同名參數(shù)涧至,鍵值應(yīng)該是一個(gè)函數(shù),會(huì)被當(dāng)作Action creator
桑包,返回的Action
會(huì)由Redux
自動(dòng)發(fā)出南蓬。
const mapDispatchToProps = {
onClick: (filter) => {
type: 'SET_VISIBILITY_FILTER',
filter: filter
};
}
- <Provider> 組件
connect
方法生成容器組件以后,需要讓容器組件拿到state
對(duì)象哑了,才能生成UI
組件的參數(shù)蓖康。
React-Redux
提供Provider
組件,使整個(gè)app
訪問到Redux store
中的數(shù)據(jù) 即state
垒手。
// src/index.tsx
import React from 'react'
import ReactDOM from 'react-dom'
import reportWebVitals from './reportWebVitals'
import { Provider } from 'react-redux'
import store from './redux/store'
import App from './App'
ReactDOM.render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>,
document.getElementById('root')
)
reportWebVitals()
- 實(shí)戰(zhàn):國(guó)際化
npm install redux react-redux react-i18next i18next --save
- redux 封裝在類組件中使用
// src/index.tsx
import React from 'react'
import ReactDOM from 'react-dom'
import './index.css'
import App from './App'
import reportWebVitals from './reportWebVitals'
import { Provider } from 'react-redux'
import store from './redux/store'
ReactDOM.render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>,
document.getElementById('root')
)
reportWebVitals();
// src/App.tsx
import React from 'react'
import { BrowserRouter, Route, Switch } from 'react-router-dom'
import styles from './App.module.css'
import { HomePage, LoginPage, DetailPage } from './pages'
import './i18n/configs'
function App() {
return (
<div className={styles.app}>
<BrowserRouter>
<Switch>
<Route exact path="/" component={HomePage} />
<Route path="/login" component={LoginPage} />
<Route path="/detail/:id" component={DetailPage} />
</Switch>
</BrowserRouter>
</div>
)
}
export default App
// src/pages/home/Home.tsx
import React from 'react'
import styles from './Home.module.css'
import { Header, Footer } from '../../components'
import { RouteComponentProps, withRouter } from 'react-router-dom'
import { withTranslation, WithTranslation } from 'react-i18next'
class HomePageComponent extends React.Component<RouteComponentProps & WithTranslation> {
render() {
const { t } = this.props
return (
<>
<Header />
<div>{t('home_page.content')}</div>
<Footer />
</>
)
}
}
export const HomePage = withTranslation()(withRouter(HomePageComponent))
// src/i18n/configs.ts
import i18n from 'i18next'
import { initReactI18next } from 'react-i18next'
import translation_en from './en.json' // 英文配置
import translation_zh from './zh.json' // 中文配置
const resources = {
en: { translation: translation_en },
zh: { translation: translation_zh },
}
i18n
.use(initReactI18next)
.init({
resources,
lng: 'zh',
interpolation: { escapeValue: false },
})
export default i18n
// src/components/header/Header.class.tsx
import React from 'react'
import { GlobalOutlined } from '@ant-design/icons'
import { Layout, Typography, Dropdown, Menu, Button, Input } from 'antd'
import styles from './Header.module.css'
import { RouteComponentProps, withRouter } from 'react-router-dom'
import { RootState } from '../../redux/store'
import { withTranslation, WithTranslation } from 'react-i18next'
import { addLanguageActionCreator, changeLanguageActionCreator } from '../../redux/language/languageActions'
import { connect } from 'react-redux'
import { Dispatch } from 'redux'
const mapStateToProps = (state: RootState) => {
return {
language: state.language,
languageList: state.languageList,
}
}
const mapDispatchToProps = (dispatch: Dispatch) => {
return {
changeLanguage: (code: 'zh' | 'en') => dispatch(changeLanguageActionCreator(code)),
addLanguage: (name: string, code: string) => dispatch(addLanguageActionCreator(name, code))
}
}
type PropsType = RouteComponentProps // react-router 路由 props 類型
& WithTranslation // i18n props 類型
& ReturnType<typeof mapStateToProps> // redux store 映射類型
& ReturnType<typeof mapDispatchToProps> // redux dispatch 映射類型
class HeaderComponent extends React.Component<PropsType> {
toggleLanguage = (event) => {
this.props.changeLanguage(event.key)
}
addLanguage = () => {
this.props.addLanguage('新語言', 'new_lang')
}
render() {
const { history, t } = this.props
return (
<div className={styles['app-header']}>
<div className={styles['top-header']}>
<div className={styles.inner}>
<Typography.Text>{t('header.slogan')}</Typography.Text>
<Dropdown.Button
style={{ marginLeft: 15 }}
overlay={
<Menu>
{
this.props.languageList.map(item => {
return <Menu.Item key={item.code} onClick={this.toggleLanguage}>{item.name}</Menu.Item>
})
}
<Menu.Item onClick={this.addLanguage}>{t('header.add_new_language')}</Menu.Item>
</Menu>
}
icon={<GlobalOutlined />}
>
{ this.props.language === 'en' ? 'English' : '中文' }
</Dropdown.Button>
<Button.Group className={styles['button-group']}>
<Button onClick={() => history.push('/register')}>{t('header.register')}</Button>
<Button onClick={() => history.push('/login')}>{t('header.signin')}</Button>
</Button.Group>
</div>
</div>
</div>
)
}
}
export const Header = connect(mapStateToProps, mapDispatchToProps)(withTranslation()(withRouter(HeaderComponent)))
// src/redux/store.ts
import { createStore } from 'redux'
import { languageReducer } from './language/languageReducer'
const store = createStore(languageReducer)
export type RootState = ReturnType<typeof store.getState>
export default store
// src/redux/language/languageActions.ts
export const CHANGE_LANGUAGE = 'changeLanguage'
export const ADD_LANGUAGE = 'addLanguage'
interface changeLanguageAction {
type: typeof CHANGE_LANGUAGE,
payload: 'zh' | 'en',
}
interface addLanguageAction {
type: typeof ADD_LANGUAGE,
payload: { name: string, code: string},
}
export type LanguageActionTypes = changeLanguageAction | addLanguageAction
export const changeLanguageActionCreator = (languageCode: 'zh' | 'en'): changeLanguageAction => {
return {
type: CHANGE_LANGUAGE,
payload: languageCode,
}
}
export const addLanguageActionCreator = (name: string, code: string): addLanguageAction => {
return {
type: ADD_LANGUAGE,
payload: { name, code },
}
}
// src/redux/language/languageReducer.ts
import i18n from 'i18next'
import { ADD_LANGUAGE, CHANGE_LANGUAGE, LanguageActionTypes } from './languageActions'
export interface LanguageState {
language: 'en' | 'zh'
languageList: { name: string, code: string }[]
}
const defaultState: LanguageState = {
language: 'zh',
languageList: [
{ name: 'English', code: 'en' },
{ name: '中文', code: 'zh' },
],
}
export const languageReducer = (state = defaultState, action: LanguageActionTypes): LanguageState => {
const { type, payload } = action
switch (type) {
case CHANGE_LANGUAGE:
i18n.changeLanguage(payload as string)
return { ...state, language: payload as 'en' | 'zh' }
case ADD_LANGUAGE:
return { ...state, languageList: [ ...state.languageList, payload as { name: string, code: string } ]}
default:
return state
}
}
- redux 封裝在函數(shù)式組件中使用
// src/redux/hooks.ts
import { useSelector, TypedUseSelectorHook } from 'react-redux'
import { RootState } from './store'
export const useReduxSelector: TypedUseSelectorHook<RootState> = useSelector
// src/components/header/Header.tsx
import React from 'react'
import { GlobalOutlined } from '@ant-design/icons'
import { Layout, Typography, Dropdown, Menu, Button, Input } from 'antd'
import styles from './Header.module.css'
import { useHistory } from 'react-router-dom'
import { useTranslation } from 'react-i18next'
import { addLanguageActionCreator, changeLanguageActionCreator } from '../../redux/language/languageActions'
import { useDispatch } from 'react-redux'
import { useReduxSelector } from '../../redux/hooks'
export const Header: React.FC = () => {
const history = useHistory()
const language = useReduxSelector(state => state.language)
const languageList = useReduxSelector(state => state.languageList)
const { t } = useTranslation()
const dispatch = useDispatch()
const toggleLanguage = (event) => {
dispatch(changeLanguageActionCreator(event.key))
}
const addLanguage = () => {
dispatch(addLanguageActionCreator('新語言', 'new_lang'))
}
return (
<div className={styles['app-header']}>
<div className={styles['top-header']}>
<div className={styles.inner}>
<Typography.Text>{t('header.slogan')}</Typography.Text>
<Dropdown.Button
style={{ marginLeft: 15 }}
overlay={
<Menu>
{
languageList.map(item => {
return <Menu.Item key={item.code} onClick={toggleLanguage}>{item.name}</Menu.Item>
})
}
<Menu.Item onClick={addLanguage}>{t('header.add_new_language')}</Menu.Item>
</Menu>
}
icon={<GlobalOutlined />}
>
{ language === 'en' ? 'English' : '中文' }
</Dropdown.Button>
<Button.Group className={styles['button-group']}>
<Button onClick={() => history.push('/register')}>{t('header.register')}</Button>
<Button onClick={() => history.push('/login')}>{t('header.signin')}</Button>
</Button.Group>
</div>
</div>
</div>
)
}