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函数连接组件]
通过这个流程图,我们可以更加清晰地看到整个集成过程的主要步骤。希望大家在实际开发中能够灵活运用这些知识,打造出更加出色的应用。
超级会员免费看
1088

被折叠的 条评论
为什么被折叠?



