27、Redux与React的深度融合:从基础到实践

Redux与React的深度融合:从基础到实践

1. Redux基础:Reducer的创建与组合

在Redux应用中,Reducer起着关键作用,它决定了状态应该如何改变。接下来,我们将创建一个处理用户登录和注销事件的Reducer,并在浏览器中存储一些Cookie,以便后续使用。

首先,我们使用 js-cookie 库来处理Cookie。以下是创建用户Reducer的代码:

import Cookies from 'js-cookie';
import initialState from '../constants/initialState';
import * as types from '../constants/types';

export function user(state = initialState.user, action) {
    switch (action.type) {
        case types.auth.LOGIN_SUCCESS:
            const { user, token } = action;
            Cookies.set('letters-token', token);
            return Object.assign({}, state.user, {
                authenticated: true,
                name: user.name,
                id: user.id,
                profilePicture: user.profilePicture || '/static/assets/users/4.jpeg',
                token
            });
        case types.auth.LOGOUT_SUCCESS:
            Cookies.remove('letters-token');
            return initialState.user;
        default:
            return state;
    }
}

上述代码中,当用户登录成功时,我们将用户的令牌存储为Cookie,并更新状态;当用户注销成功时,我们移除Cookie并将用户状态重置为初始状态。

接下来,我们需要将这些Reducer集成到Redux存储中。以下是将新创建的Reducer添加到根Reducer的代码:

import { combineReducers } from 'redux';
import { error } from './error';
import { loading } from './loading';
import { pagination } from './pagination';
import { posts, postIds } from './posts';
import { user } from './user';
import { comments, commentIds } from './comments';

const rootReducer = combineReducers({
    commentIds,
    comments,
    error,
    loading,
    pagination,
    postIds,
    posts,
    user
});

export default rootReducer;

这里使用 combineReducers 函数将多个Reducer组合在一起,每个Reducer负责管理存储状态的一个特定部分。

2. Redux Reducer的测试

由于Redux Reducer是纯函数,因此测试它们非常简单。我们可以通过给定特定的输入,断言它们应该产生特定的状态。以下是测试用户Reducer的代码:

jest.mock('js-cookie');
import Cookies from 'js-cookie';
import { user } from '../../src/reducers/user';
import initialState from '../../src/constants/initialState';
import * as types from '../../src/constants/types';

describe('user', () => {
    test('should return the initial state', () => {
        expect(user(initialState.user, {})).toEqual(initialState.user);
    });

    test(`${types.auth.LOGIN_SUCCESS}`, () => {
        const mockUser = {
            name: 'name',
            id: 'id',
            profilePicture: 'pic'
        };
        const mockToken = 'token';
        const expectedState = {
            name: 'name',
            id: 'id',
            profilePicture: 'pic',
            token: mockToken,
            authenticated: true
        };
        expect(
            user(initialState.user, {
                type: types.auth.LOGIN_SUCCESS,
                user: mockUser,
                token: mockToken
            })
        ).toEqual(expectedState);
        expect(Cookies).toHaveBeenCalled();
    });

    test(`${types.auth.LOGOUT_SUCCESS}, browser`, () => {
        expect(
            user(initialState.user, {
                type: types.auth.LOGOUT_SUCCESS
            })
        ).toEqual(initialState.user);
        expect(Cookies).toHaveBeenCalled();
    });
});

上述代码中,我们使用Jest框架对用户Reducer进行了测试,分别测试了初始状态、登录成功和注销成功的情况。

3. React与Redux的集成

虽然我们已经完成了Redux的设置,但React组件目前还对其一无所知。接下来,我们将使用 react-redux 库将它们结合在一起。

3.1 容器组件与展示组件

在将Redux集成到React应用中时,我们会遇到两种类型的组件:展示组件和容器组件。

展示组件主要处理UI和与UI相关的数据,它们通常不涉及应用数据的更改、更新或发出。以下是展示组件的一些特点:
- 处理事物的外观,而不是数据的流动或确定方式。
- 仅在必要时拥有自己的状态,大多数情况下应该是无状态的函数组件,通过 react-redux 绑定从Redux接收属性。
- 当它们拥有自己的状态时,应该是与UI相关的数据,而不是应用数据。
- 不决定数据的加载或更改方式,这主要应该在容器组件中完成。
- 通常是手动创建的,而不是由 react-redux 库创建的。
- 可能包含样式信息,如CSS类、其他与样式相关的组件以及任何其他与UI相关的数据。

容器组件则处理应用数据,它们作为数据源,可以是有状态的,状态通常来自Redux存储。以下是容器组件的一些特点:
- 作为数据源,可以是有状态的,状态通常来自Redux存储。
- 向展示组件提供数据和行为信息(如操作)。
- 可以包含其他展示或容器组件,通常一个容器是有许多展示子组件的父组件。
- 通常使用 react-redux connect 方法创建,并且通常是高阶组件。
- 通常不包含与应用数据无关的样式信息。

为了将组件连接到Redux存储,我们可以采取以下步骤:
1. 除了常规组件外,导出一个连接的组件。
2. 将任何属性和状态移动到 react-redux 可以使用的特殊函数中。
3. 引入所需的操作并将它们绑定到组件将拥有的 actions 属性上。
4. 在适当的地方用映射到Redux存储状态的属性替换本地状态。

3.2 使用 <Provider /> 连接组件到Redux存储

将Redux设置集成到React应用中的第一步是使用 react-redux 提供的 <Provider /> 组件包裹整个应用。以下是使用 <Provider /> 组件的代码:

import React from 'react';
import { render } from 'react-dom';
import { Provider } from 'react-redux';
import Firebase from 'firebase';
import * as API from './shared/http';
import { history } from './history';
import configureStore from './store/configureStore';
import initialReduxState from './constants/initialState';
import Route from './components/router/Route';
import Router from './components/router/Router';
import App from './app';
import Home from './pages/home';
import SinglePost from './pages/post';
import Login from './pages/login';
import NotFound from './pages/404';
import { createError } from './actions/error';
import { loginSuccess } from './actions/auth';
import { loaded, loading } from './actions/loading';
import { getFirebaseUser, getFirebaseToken } from './backend/auth';
import './shared/crash';
import './shared/service-worker';
import './shared/vendor';
import './styles/styles.scss';

const store = configureStore(initialReduxState);
const renderApp = (state, callback = () => {}) => {
    render(
        <Provider store={store}>
            <Router {...state}>
                <Route path="" component={App}>
                    <Route path="/" component={Home} />
                    <Route path="/posts/:postId" component={SinglePost} />
                    <Route path="/login" component={Login} />
                    <Route path="*" component={NotFound} />
                </Route>
            </Router>
        </Provider>,
        document.getElementById('app'),
        callback
    );
};

const initialState = {
    location: window.location.pathname
};

// Render the app initially
renderApp(initialState);

history.listen(location => {
    const user = Firebase.auth().currentUser;
    const newState = Object.assign(initialState, { location: user ? location.pathname : '/login' });
    renderApp(newState);
});

getFirebaseUser()
    .then(async user => {
        if (!user) {
            return history.push('/login');
        }
        store.dispatch(loading());
        const token = await getFirebaseToken();
        const res = await API.loadUser(user.uid);
        if (res.status === 404) {
            const userPayload = {
                name: user.displayName,
                profilePicture: user.photoURL,
                id: user.uid
            };
            const newUser = await API.createUser(userPayload).then(res => res.json());
            store.dispatch(loginSuccess(newUser, token));
            store.dispatch(loaded());
            history.push('/');
            return newUser;
        }
        const existingUser = await res.json();
        store.dispatch(loginSuccess(existingUser, token));
        store.dispatch(loaded());
        history.push('/');
        return existingUser;
    })
    .catch(err => createError(err));

上述代码中,我们使用 <Provider /> 组件将整个应用包裹起来,并将Redux存储传递给它,这样连接的组件就可以访问存储了。

3.3 使用 connect 函数连接组件

为了将组件连接到Redux存储,我们可以使用 react-redux connect 函数。以下是使用 connect 函数的代码:

import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import orderBy from 'lodash/orderBy';
import Ad from '../components/ad/Ad';
import CreatePost from '../components/post/Create';
import Post from '../components/post/Post';
import Welcome from '../components/welcome/Welcome';

export class Home extends Component {
    render() {
        return (
            <div className="home">
                <Welcome />
                <div>
                    <CreatePost />
                    {this.props.posts && (
                        <div className="posts">
                            {this.props.posts.map(post => (
                                <Post
                                    key={post.id}
                                    post={post}
                                />
                            ))}
                        </div>
                    )}
                    <button className="block">
                        Load more posts
                    </button>
                </div>
                <div>
                    <Ad url="https://ifelse.io/book" imageUrl="/static/assets/ads/ria.png" />
                    <Ad url="https://ifelse.io/book" imageUrl="/static/assets/ads/orly.jpg" />
                </div>
            </div>
        );
    }
}

export const mapStateToProps = state => {
    const posts = orderBy(state.postIds.map(postId => state.posts[postId]), 'date', 'desc');
    return { posts };
};

export default connect(mapStateToProps)(Home);

上述代码中,我们使用 connect 函数将 Home 组件连接到Redux存储,并使用 mapStateToProps 函数将存储状态映射到组件的属性上。

通过以上步骤,我们可以将Redux与React完美地结合在一起,实现一个可预测的状态管理和高效的组件通信。

Redux与React的深度融合:从基础到实践

4. 组件连接过程总结与优化建议

在完成了上述的组件连接步骤后,我们可以总结一下整个过程,同时给出一些优化建议,以帮助大家更好地使用Redux和React。

4.1 组件连接过程总结
步骤 描述 代码示例
创建Reducer 处理状态变化,如用户登录和注销 javascript<br>import Cookies from 'js-cookie';<br>import initialState from '../constants/initialState';<br>import * as types from '../constants/types';<br><br>export function user(state = initialState.user, action) {<br> switch (action.type) {<br> case types.auth.LOGIN_SUCCESS:<br> const { user, token } = action;<br> Cookies.set('letters-token', token);<br> return Object.assign({}, state.user, {<br> authenticated: true,<br> name: user.name,<br> id: user.id,<br> profilePicture: user.profilePicture || '/static/assets/users/4.jpeg',<br> token<br> });<br> case types.auth.LOGOUT_SUCCESS:<br> Cookies.remove('letters-token');<br> return initialState.user;<br> default:<br> return state;<br> }<br>}<br>
组合Reducer 将多个Reducer组合成根Reducer javascript<br>import { combineReducers } from 'redux';<br>import { error } from './error';<br>import { loading } from './loading';<br>import { pagination } from './pagination';<br>import { posts, postIds } from './posts';<br>import { user } from './user';<br>import { comments, commentIds } from './comments';<br><br>const rootReducer = combineReducers({<br> commentIds,<br> comments,<br> error,<br> loading,<br> pagination,<br> postIds,<br> posts,<br> user<br>});<br><br>export default rootReducer;<br>
测试Reducer 确保Reducer的正确性 javascript<br>jest.mock('js-cookie');<br>import Cookies from 'js-cookie';<br>import { user } from '../../src/reducers/user';<br>import initialState from '../../src/constants/initialState';<br>import * as types from '../../src/constants/types';<br><br>describe('user', () => {<br> test('should return the initial state', () => {<br> expect(user(initialState.user, {})).toEqual(initialState.user);<br> });<br><br> test(`${types.auth.LOGIN_SUCCESS}`, () => {<br> const mockUser = {<br> name: 'name',<br> id: 'id',<br> profilePicture: 'pic'<br> };<br> const mockToken = 'token';<br> const expectedState = {<br> name: 'name',<br> id: 'id',<br> profilePicture: 'pic',<br> token: mockToken,<br> authenticated: true<br> };<br> expect(<br> user(initialState.user, {<br> type: types.auth.LOGIN_SUCCESS,<br> user: mockUser,<br> token: mockToken<br> })<br> ).toEqual(expectedState);<br> expect(Cookies).toHaveBeenCalled();<br> });<br><br> test(`${types.auth.LOGOUT_SUCCESS}, browser`, () => {<br> expect(<br> user(initialState.user, {<br> type: types.auth.LOGOUT_SUCCESS<br> })<br> ).toEqual(initialState.user);<br> expect(Cookies).toHaveBeenCalled();<br> });<br>});<br>
使用 <Provider /> 将Redux存储提供给React组件 javascript<br>import React from 'react';<br>import { render } from 'react-dom';<br>import { Provider } from 'react-redux';<br>import Firebase from 'firebase';<br>import * as API from './shared/http';<br>import { history } from './history';<br>import configureStore from './store/configureStore';<br>import initialReduxState from './constants/initialState';<br>import Route from './components/router/Route';<br>import Router from './components/router/Router';<br>import App from './app';<br>import Home from './pages/home';<br>import SinglePost from './pages/post';<br>import Login from './pages/login';<br>import NotFound from './pages/404';<br>import { createError } from './actions/error';<br>import { loginSuccess } from './actions/auth';<br>import { loaded, loading } from './actions/loading';<br>import { getFirebaseUser, getFirebaseToken } from './backend/auth';<br>import './shared/crash';<br>import './shared/service-worker';<br>import './shared/vendor';<br>import './styles/styles.scss';<br><br>const store = configureStore(initialReduxState);<br>const renderApp = (state, callback = () => {}) => {<br> render(<br> <Provider store={store}><br> <Router {...state}><br> <Route path="" component={App}><br> <Route path="/" component={Home} /><br> <Route path="/posts/:postId" component={SinglePost} /><br> <Route path="/login" component={Login} /><br> <Route path="*" component={NotFound} /><br> </Route><br> </Router><br> </Provider>,<br> document.getElementById('app'),<br> callback<br> );<br>};<br><br>const initialState = {<br> location: window.location.pathname<br>};<br><br>// Render the app initially<br>renderApp(initialState);<br><br>history.listen(location => {<br> const user = Firebase.auth().currentUser;<br> const newState = Object.assign(initialState, { location: user ? location.pathname : '/login' });<br> renderApp(newState);<br>});<br><br>getFirebaseUser()<br> .then(async user => {<br> if (!user) {<br> return history.push('/login');<br> }<br> store.dispatch(loading());<br> const token = await getFirebaseToken();<br> const res = await API.loadUser(user.uid);<br> if (res.status === 404) {<br> const userPayload = {<br> name: user.displayName,<br> profilePicture: user.photoURL,<br> id: user.uid<br> };<br> const newUser = await API.createUser(userPayload).then(res => res.json());<br> store.dispatch(loginSuccess(newUser, token));<br> store.dispatch(loaded());<br> history.push('/');<br> return newUser;<br> }<br> const existingUser = await res.json();<br> store.dispatch(loginSuccess(existingUser, token));<br> store.dispatch(loaded());<br> history.push('/');<br> return existingUser;<br> })<br> .catch(err => createError(err));<br>
使用 connect 函数 将组件连接到Redux存储 javascript<br>import PropTypes from 'prop-types';<br>import React, { Component } from 'react';<br>import { connect } from 'react-redux';<br>import orderBy from 'lodash/orderBy';<br>import Ad from '../components/ad/Ad';<br>import CreatePost from '../components/post/Create';<br>import Post from '../components/post/Post';<br>import Welcome from '../components/welcome/Welcome';<br><br>export class Home extends Component {<br> render() {<br> return (<br> <div className="home"><br> <Welcome /><br> <div><br> <CreatePost /><br> {this.props.posts && (<br> <div className="posts"><br> {this.props.posts.map(post => (<br> <Post<br> key={post.id}<br> post={post}<br> /> ))}<br> </div><br> )}<br> <button className="block"><br> Load more posts<br> </button><br> </div><br> <div><br> <Ad url="https://ifelse.io/book" imageUrl="/static/assets/ads/ria.png" /><br> <Ad url="https://ifelse.io/book" imageUrl="/static/assets/ads/orly.jpg" /><br> </div><br> </div><br> );<br> }<br>}<br><br>export const mapStateToProps = state => {<br> const posts = orderBy(state.postIds.map(postId => state.posts[postId]), 'date', 'desc');<br> return { posts };<br>};<br><br>export default connect(mapStateToProps)(Home);<br>
4.2 优化建议
  • 减少不必要的状态更新 :在 mapStateToProps 函数中,尽量只返回组件需要的状态,避免返回过多的状态导致组件不必要的重新渲染。
  • 使用 shouldComponentUpdate 生命周期方法 :对于展示组件,可以使用 shouldComponentUpdate 方法来控制组件是否需要重新渲染,提高性能。
  • 使用中间件处理异步操作 :对于异步操作,如网络请求,可以使用Redux中间件(如 redux-thunk redux-saga )来处理,使代码更加清晰和可维护。
5. 总结与展望

通过本文的介绍,我们详细了解了如何将Redux与React结合在一起,实现一个可预测的状态管理和高效的组件通信。从创建Reducer、组合Reducer、测试Reducer,到使用 <Provider /> connect 函数将组件连接到Redux存储,每一个步骤都至关重要。

同时,我们也总结了组件连接过程,并给出了一些优化建议,希望能够帮助大家更好地使用Redux和React。

在未来的开发中,随着项目的不断扩大,我们可能会遇到更多的挑战和问题。例如,如何处理复杂的异步操作、如何优化性能等。但是,只要我们掌握了Redux和React的基本原理和使用方法,就能够应对这些挑战,开发出更加优秀的应用程序。

下面是一个简单的mermaid流程图,展示了Redux与React集成的主要步骤:

graph LR
    A[创建Reducer] --> B[组合Reducer]
    B --> C[测试Reducer]
    C --> D[使用<Provider />提供存储]
    D --> E[使用connect函数连接组件]

通过这个流程图,我们可以更加清晰地看到整个集成过程的主要步骤。希望大家在实际开发中能够灵活运用这些知识,打造出更加出色的应用。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值