React
实际上只是 UI
框架,通过 JSX
生成动态 dom 渲染 UI,没有架构、没有模板、没有设计模式、没有路由、也没有数据管理。所以需要借助其他工具。
redux
npm install redux --save
-
什么是 redux ?
Redux
是JavaScript
状态容器,提供可预测化的状态管理。可以理解为全局数据状态管理工具,用来做组件通信等。 -
为什么使用 redux ?
当没有使用redux
时兄弟组件间传值将很麻烦,代码很复杂冗余。使用redux
定义全局单一的数据Store
,可以自定义Store
里面存放哪些数据,整个数据结构也是自己清楚的。 -
redux 工作流 ?
redux 工作流- store:推送数据的仓库
- reducer:帮助 store 处理数据的方法(初始化、修改、删除)
- actions:数据更新的指令
- react 组件(UI):订阅 store 中的数据
-
redux 用法:
import { createStore } from 'redux'
/*
* 这是一个 reducer,形式为 (state, action) => state 的纯函数。描述了 action 如何把 state 转变成下一个 state。
* state 的形式取决于你,可以是基本类型、数组、对象、甚至是 Immutable.js 生成的数据结构。
* 当 state 变化时需要返回全新的对象,而不是修改传入的参数。
*/
function counter(state = 0, action) {
switch (action.type) {
case 'INCREMENT':
return state + 1
case 'DECREMENT':
return state - 1
default:
return state;
}
}
// 创建 Redux store 来存放应用的状态
// API 是 { subscribe, dispatch, getState }
const store = createStore(counter);
// 可以手动订阅更新,也可以事件绑定到视图层。
store.subscribe(() =>
const sotreState = store.getState()
......
)
// 改变内部 state 惟一方法是 dispatch 一个 action。
store.dispatch({ type: 'INCREMENT' })
store.dispatch({ type: 'DECREMENT' })
-
redux 三大原则:
-
单一数据源:整个应用的 state 存放在唯一的一个 store 中。
store.getState()
-
state 是只读的,唯一改变 state 的方法就是触发 action,action 是一个用于描述已发生事件的普通对象。
store.dispatch({ type: 'COMPLETE_TODO', index: 1 })
- 使用纯函数来执行修改(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
中读取数据,并且向 store
分发 actions
以更新数据
-
React-Redux
将所有组件分成两大类:UI
组件和容器组件。UI
组件负责UI
的呈现,容器组件负责管理数据和逻辑。-
UI
组件:只负责 UI 的呈现,不带有任何业务逻辑;没有状态(即不使用this.state
这个变量);所有数据都由参数this.props
提供;不使用任何Redux
的API
- 容器组件:负责管理数据和业务逻辑,不负责
UI
的呈现;带有内部状态;使用Redux
的API
。
-
-
React-Redux
规定,所有的UI
组件都由用户提供,容器组件则是由React-Redux
自动生成。也就是说,用户负责视觉层,状态管理则是全部交给它。 -
connect()
import { connect } from 'react-redux'
const VisibleTodoList = connect(mapStateToProps, mapDispatchToProps)(TodoList)
上面 VisibleTodoList
便是 UI
组件 TodoList
通过 connect
方法自动生成的容器组件。
connect
方法接受两个参数:mapStateToProps
和 mapDispatchToProps
。它们定义了 UI
组件的业务逻辑。前者负责输入逻辑,即将 state
映射到 UI
组件的参数 props
,后者负责输出逻辑,即将用户对 UI
组件的操作映射成 Action
。
- mapStateToProps()
const mapStateToProps = (state) => {
return {
todos: getVisibleTodos(state.todos, state.visibilityFilter)
}
}
mapStateToProps
是一个函数,它接受 state
作为参数,返回一个对象。这个对象有一个 todos
属性,代表 UI
组件的同名参数,后面的 getVisibleTodos
也是一个函数,可以从 state
算出 todos
的值。
mapStateToProps
建立一个从(外部的)state
对象到(UI
组件的)props
对象的映射关系。执行后应该返回一个对象,里面的每一个键值对就是一个映射。
- mapDispatchToProps()
mapDispatchToProps
用来建立UI
组件的参数到store.dispatch
方法的映射。它定义了哪些用户的操作应该当作Action
,传给Store
。它可以是一个函数,也可以是一个对象。
- 是函数则会得到
dispatch
和ownProps
(容器组件的props
对象)两个参数。
const mapDispatchToProps = (dispatch, ownProps) => {
return {
onClick: () => {
dispatch({
type: 'SET_VISIBILITY_FILTER',
filter: ownProps.filter,
})
}
}
}
- 是一个对象,它的每个键名也是对应
UI
组件的同名参数,键值应该是一个函数,会被当作Action creator
,返回的Action
会由Redux
自动发出。
const mapDispatchToProps = {
onClick: (filter) => {
type: 'SET_VISIBILITY_FILTER',
filter: filter
};
}
- <Provider> 组件
connect
方法生成容器组件以后,需要让容器组件拿到state
对象,才能生成UI
组件的参数。
React-Redux
提供Provider
组件,使整个app
访问到Redux store
中的数据 即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()
- 实战:国际化
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 封装在函数式组件中使用
// 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>
)
}
网友评论