React-Redux

作者: 欢欣的膜笛 | 来源:发表于2021-01-29 00:22 被阅读0次

    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>
        )
    }
    

    相关文章

      网友评论

        本文标题:React-Redux

        本文链接:https://www.haomeiwen.com/subject/rujftltx.html