React-Redux

React 实际上只是 UI 框架,通过 JSX 生成动态 dom 渲染 UI,没有架构、没有模板、没有设计模式、没有路由、也没有数据管理。所以需要借助其他工具。

redux

npm install redux --save

  1. 什么是 redux ?
    ReduxJavaScript 状态容器,提供可预测化的状态管理。可以理解为全局数据状态管理工具,用来做组件通信等。

  2. 为什么使用 redux ?
    当没有使用 redux 时兄弟组件间传值将很麻烦,代码很复杂冗余。使用 redux 定义全局单一的数据 Store,可以自定义 Store 里面存放哪些数据,整个数据结构也是自己清楚的。

  3. redux 工作流 ?


    redux 工作流
    • store:推送数据的仓库
    • reducer:帮助 store 处理数据的方法(初始化、修改、删除)
    • actions:数据更新的指令
    • react 组件(UI):订阅 store 中的数据
  4. 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' })
  1. 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-ReduxRedux 的官方 React 绑定库。它能够使你的 React 组件从 Redux store 中读取数据,并且向 store 分发 actions 以更新数据

  1. React-Redux 将所有组件分成两大类:UI 组件和容器组件。UI 组件负责 UI 的呈现,容器组件负责管理数据和逻辑。

    • UI 组件:只负责 UI 的呈现,不带有任何业务逻辑;没有状态(即不使用 this.state 这个变量);所有数据都由参数 this.props 提供;不使用任何 ReduxAPI
    • 容器组件:负责管理数据和业务逻辑,不负责 UI 的呈现;带有内部状态;使用 ReduxAPI
  2. React-Redux 规定,所有的 UI 组件都由用户提供,容器组件则是由 React-Redux 自动生成。也就是说,用户负责视觉层,状态管理则是全部交给它。

  3. connect()

import { connect } from 'react-redux'
const VisibleTodoList = connect(mapStateToProps, mapDispatchToProps)(TodoList)

上面 VisibleTodoList 便是 UI 组件 TodoList 通过 connect 方法自动生成的容器组件。

connect 方法接受两个参数:mapStateToPropsmapDispatchToProps。它们定义了 UI 组件的业务逻辑。前者负责输入逻辑,即将 state 映射到 UI 组件的参数 props,后者负责输出逻辑,即将用户对 UI 组件的操作映射成 Action

  1. mapStateToProps()
const mapStateToProps = (state) => {
  return {
    todos: getVisibleTodos(state.todos, state.visibilityFilter)
  }
}

mapStateToProps 是一个函数,它接受 state 作为参数,返回一个对象。这个对象有一个 todos 属性,代表 UI 组件的同名参数,后面的 getVisibleTodos 也是一个函数,可以从 state 算出 todos 的值。
mapStateToProps 建立一个从(外部的)state 对象到(UI 组件的)props 对象的映射关系。执行后应该返回一个对象,里面的每一个键值对就是一个映射。

  1. mapDispatchToProps()
    mapDispatchToProps 用来建立 UI 组件的参数到 store.dispatch 方法的映射。它定义了哪些用户的操作应该当作 Action,传给 Store。它可以是一个函数,也可以是一个对象。
  • 是函数则会得到 dispatchownProps(容器组件的 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
  };
}
  1. <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()
  1. 实战:国际化
    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>
    )
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,881评论 4 368
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,052评论 1 301
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 110,598评论 0 250
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,407评论 0 217
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,823评论 3 294
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,872评论 1 224
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,037评论 2 317
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,778评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,505评论 1 247
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,745评论 2 253
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,233评论 1 264
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,568评论 3 260
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,231评论 3 241
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,141评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,939评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,954评论 2 283
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,784评论 2 275

推荐阅读更多精彩内容