在 React 中管理状态:Redux 还是非 Redux?
注意:这篇文章最初发布在marmelab.com上。
在 Marmelab,我们非常喜欢使用Redux来管理 React 应用的状态。它的出现彻底改变了我们编写应用程序的方式:不变性、函数式编程、使用 Redux-Saga 生成器管理异步 API 调用……以至于我们有时倾向于将 Redux “事实上” 集成到项目启动栈中。
但这是个好主意吗?我不确定……
示例:使用 React 管理聚会
让我们以一个简单的聚会管理应用程序为例。它应该能够显示:
- 提案清单,
- 一份希望讨论的清单,
- 聚会成员名单。
数据来自 REST API。登录名/密码可同时保护应用程序和 API。
该应用程序使用Create React App引导并使用以下方式升级:
该项目如下所示:
该应用程序体现了典型的 Redux 架构。它以<App />
挂载 Redux Store ( <Provider store={store}>
) 和 Router ( <ConnectedRouter history={history}>
) 的组件开始:
// in App.js
...
export const App = ({ store, history }) => (
<Provider store={store}>
<ConnectedRouter history={history}>
<Container>
<Header />
<Switch>
<Route exact path="/" component={Home} />
<Route path="/talks" component={Talks} />
<Route path="/wishes" component={Wishes} />
<Route path="/members" component={Members} />
<Route path="/login" component={Authentication} />
<Route component={NoMatch} />
</Switch>
</Container>
</ConnectedRouter>
</Provider>
);
Redux 用户应该会对我选择的文件结构感到满意。我将与某个功能相关的所有代码分组到了一个目录中。以下talks
页面示例:
├── talks
│ ├── actions.js
│ ├── reducer.js
│ ├── sagas.js
│ └── Talks.js
页面<Talks>
组件是一个简单的“连接组件”:
// in talks/Talks.js
export const Talks = ({ isLoading, talks }) => (
<div>
<h1>Talks</h1>
{isLoading && <Spinner />}
{talks && talks.map(talk => <h2 key={talk.id}>{talk.title}</h2>)}
</div>
);
const mapStateToProps = ({ talks }) => ({
isLoading: talks.isLoading,
talks: talks.data,
});
// passing {} as the second's connect argument prevents it to pass dispatch as prop
const mapDispatchToProps = {};
export default connect(mapStateToProps, mapDispatchToProps)(Talks);
会谈的数据不是通过获取的componentWillMount
,而是通过监听路线变化的 saga 获取的:
// in talks/sagas.js
import { put, select, takeLatest } from 'redux-saga/effects';
import { LOCATION_CHANGE } from 'react-router-redux';
import { loadTalks } from './actions';
const hasData = ({ talks }) => !!talks.data;
export function* handleTalksLoading() {
if (yield select(hasData)) {
return;
}
yield put(loadTalks());
}
export const sagas = function*() {
yield takeLatest(
action =>
action.type === LOCATION_CHANGE &&
action.payload.pathname === '/talks',
handleTalksLoading,
);
};
当路线发生变化并对应于会谈部分(action.type === LOCATION_CHANGE && action.payload.pathname === '/talks'
)时,我的应用程序将使用以下功能触发操作loadTalks
:
// in talks/actions.js
export const LOAD_TALKS = 'LOAD_TALKS';
export const loadTalks = payload => ({
type: 'LOAD_TALKS',
payload,
meta: {
request: {
url: '/talks',
},
},
});
此操作包含获取其元数据中的对话数据的 url ,将被通用的 fetch saga 拦截action => !!action.meta && action.meta.request
:
// in /services/fetch/fetchSagas.js
import { call, put, takeEvery, select } from 'redux-saga/effects';
import { appFetch as fetch } from './fetch';
export const fetchError = (type, error) => ({
type: `${type}_ERROR`,
payload: error,
meta: {
disconnect: error.code === 401,
},
});
export const fetchSuccess = (type, response) => ({
type: `${type}_SUCCESS`,
payload: response,
});
export function* executeFetchSaga({ type, meta: { request } }) {
const token = yield select(state => state.authentication.token);
const { error, response } = yield call(fetch, request, token);
if (error) {
yield put(fetchError(type, error));
return;
}
yield put(fetchSuccess(type, response));
}
export const sagas = function*() {
yield takeEvery(
action => !!action.meta && action.meta.request,
executeFetchSaga,
);
};
一旦获取成功,saga 会触发最终操作,表明数据恢复成功( )。此操作由talkreducercreateAction('${type}_SUCCESS')(response)
使用:
// in talks/reducers.js
export const reducer = (state = defaultState, action) => {
switch (action.type) {
case LOAD_TALKS:
return {
...state,
loading: true,
};
case LOAD_TALKS_ERROR:
return {
...state,
loading: false,
error: action.payload,
};
case LOAD_TALKS_SUCCESS:
return {
...state,
loading: false,
data: action.payload,
};
case LOGOUT:
return defaultState;
default:
return state;
}
};
效果很好。这真是太巧妙了,甚至可以说是优雅!使用 action 的meta 属性,可以在应用内共享通用行为(例如数据获取、错误处理和注销)。
它很聪明,但很复杂
当你发现这个应用时,找到它的用法并不容易,有些行为简直太神奇了。总结一下,应用使用连接到路由器的 redux-saga 获取数据,路由器发送一个获取操作,该操作会被另一个通用 saga 拦截,如果成功,该 saga 会发出另一个操作,而这个操作会被页面的 reducer 拦截,该 reducer 已经发出了链中的第一个操作……
有些人可能会说这是对 redux 的滥用,但这主要是在这个堆栈上完成的几个项目的结果,具有重写动作和 reducer 的经验。
除了这种复杂性之外,还有大量的管道,即每个功能(动作、减速器和其他传奇)重复的许多文件。
让我们分析一下示例应用程序的三个页面:主页和登录页:
❯ cloc services/cra_webapp/src
32 text files.
32 unique files.
0 files ignored.
github.com/AlDanial/cloc v 1.74 T=0.06 s (581.6 files/s, 17722.1 lines/s)
-------------------------------------------------------------------------------
Language files blank comment code
-------------------------------------------------------------------------------
JavaScript 31 150 1 819
CSS 1 0 0 5
-------------------------------------------------------------------------------
SUM: 32 150 1 824
-------------------------------------------------------------------------------
31个文件,819行代码,对于一个简单的应用程序来说,这已经够多了。这段代码可以稍微简化一下,但可能会降低其通用性。
现在确实是时候问自己一下,Redux 是否有必要?
Redux 是 JavaScript 应用程序的可预测状态容器。
但是,应用程序的不同部分是否会修改相同的数据,从而需要这些数据具有可预测的状态?不,我只需要显示来自 API 的数据。DOM 中是否存在可以修改数据的组件?不,用户交互非常有限。
所以我可能不需要 Redux。
不使用 Redux 获取数据
让我们尝试不使用 Redux 来获取数据,或者更确切地说,不使用Redux-Saga(因为 Redux 并不直接负责数据获取)。我可以在每个页面上实现所有这些获取逻辑。然而,这会引入非常重复的机制和大量重复的代码。所以我必须找到一种通用的方法来从 API 中获取数据,而不会引入太多的重复和复杂性。
渲染道具模式是解决此类问题的绝佳选择!
让我们创建一个DataProvider
组件:
// in DataProvider.js
import React, { Component, Fragment } from 'react';
import { Redirect } from 'react-router';
import { appFetch } from './services/fetch';
export class DataProvider extends Component {
static propTypes = {
render: PropTypes.func.isRequired,
url: PropTypes.string.isRequired,
};
state = {
data: undefined,
error: undefined,
};
fetchData = async props => {
const token = window.sessionStorage.getItem('token');
try {
const data = await appFetch({ url }, token);
this.setState({
data: data.response,
error: null,
});
} catch (error) {
this.setState({
error,
});
}
};
componentDidMount() {
return this.fetchData(this.props);
}
render() {
const { data, error } = this.state;
const { location } = this.props;
if (error) {
return error.code >= 401 && error.code <= 403 ? (
<Redirect to="/login" />
) : (
<p>Erreur lors du chargement des données</p>
);
}
return (
<Fragment>
{data ? (
<p>Aucune donnée disponible</p>
) : (
this.props.render({
data,
})
)}
</Fragment>
);
}
}
url
此组件在 期间从 prop 获取数据。它管理错误和缺失数据。如果获取到数据,它会将渲染委托给通过prop ( )componentDidMount
传递的函数。render
this.props.render({ data })
让我们在讨论页面上实现这个组件:
// in talks/Talks.js
import React from 'react';
import PropTypes from 'prop-types';
import { DataProvider } from '../DataProvider';
export const TalksView = ({ talks }) => (
<div>
<h1>Talks</h1>
{talks && talks.map(talk => <h2 key={talk.id}>{talk.title}</h2>)}
</div>
);
TalksView.propTypes = {
talks: PropTypes.array,
};
export const Talks = () => (
<DataProvider
url="/talks"
render={({ data }) => <TalksView talks={data} />}
/>
);
我现在有两个组件:
- 该
TalksView
组件仅显示数据,无论数据来自哪里, - 组件
Talks
,使用它DataProvider
来获取数据并TalksView
显示它render={({ data }) => <TalksView talks={data} />}
。
它简单、有效、易读!
有一个优秀的库实现了这种类型的 DataProvider:react-request:React 的声明式 HTTP 请求
我现在准备从应用程序中删除 Redux。
让我们重新开始我们的项目分析:
❯ cloc services/cra_webapp/src
16 text files.
16 unique files.
0 files ignored.
github.com/AlDanial/cloc v 1.74 T=0.04 s (418.9 files/s, 13404.6 lines/s)
-------------------------------------------------------------------------------
Language files blank comment code
-------------------------------------------------------------------------------
JavaScript 15 64 1 442
CSS 1 0 0 5
-------------------------------------------------------------------------------
SUM: 16 64 1 447
-------------------------------------------------------------------------------
所以我把代码从 819 行缩减到了442 行,几乎减少了一半。还不错!
用 React State 替换 Redux Store
目前,每个页面都使用 DataProvider 获取数据。但是,我的应用程序需要身份验证才能通过json-web-token获取用户信息。
在没有 Redux 存储的情况下,如何将这些用户信息传输到各个组件?嗯,通过使用更高级别组件的状态App.js
( ),并将作为 prop 传递user
给需要它的子组件(PrivateRoute.js
,Header.js
)。
简而言之,让我们重新编写 React 代码吧!
// in App.js
import React, { Component } from 'react';
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
import { Authentication } from './authentication/Authentication';
import { Header } from './components/Header';
import { PrivateRoute } from './PrivateRoute';
import { Talks } from './talks/Talks';
export class App extends Component {
state = {
user: null,
};
decodeToken = token => {
const user = decode(token);
this.setState({ user });
};
componentWillMount() {
const token = window.sessionStorage.getItem('token');
if (token) {
this.decodeToken(token);
}
}
handleNewToken = token => {
window.sessionStorage.setItem('token', token);
this.decodeToken(token);
};
handleLogout = () => {
window.sessionStorage.removeItem('token');
this.setState({ user: null });
};
render() {
const { user } = this.state;
return (
<Router>
<div>
<Header user={user} onLogout={this.handleLogout} />
<Switch>
<PrivateRoute
path="/talks"
render={() => (
<Talks />
)}
user={user}
/>
<Route
path="/login"
render={({ location }) => (
<Authentication
location={location}
onNewToken={this.handleNewToken}
/>
)}
/>
</Switch>
</div>
</Router>
);
}
}
注意:我知道:存储token
inwindow.sessionStorage
是一种不好的做法。但这允许我为了本示例快速设置身份验证。这与 Redux 的移除无关。
// in PrivateRoute.js
import React from 'react';
import PropTypes from 'prop-types';
import { Redirect, Route } from 'react-router';
/**
* This Route will redirect the user to the login page if needed.
*/
export const PrivateRoute = ({ user, ...rest }) =>
user ? (
<Route {...rest} />
) : (
<Redirect
to={{
pathname: '/login',
state: { from: rest.location },
}}
/>
);
PrivateRoute.propTypes = {
user: PropTypes.object,
};
// in components/Header.js
import React from 'react';
import PropTypes from 'prop-types';
import { Navigation } from './Navigation';
export const Header = ({ user, onLogout }) => (
<header>
<h1>JavaScript Playground: meetups</h1>
{user && <Navigation onLogout={onLogout} />}
</header>
);
Header.propTypes = {
user: PropTypes.object,
onLogout: PropTypes.func.isRequired,
};
我的应用程序相对简单,将其user
作为道具传递给孩子们并不是什么问题。
假设我想让导航栏更漂亮,并添加一个显示用户名的真正的注销菜单。我必须将这个参数传递user
给Navigation
组件。
<Navigation onLogout={onLogout} user={user}/>
此外,如果<UserMenu>
组件使用另一个组件来显示用户,我将不得不再次传输我的用户:
const UserMenu = ({ onLogout, user }) => {
<div>
<DisplayUser user={user} />
<UserSubMenu onLogout={onLogout} />
</div>
}
user
在显示之前已经经过了 4 个组件...
那么更复杂和/或更重的应用怎么办?这会变得非常痛苦。在这种情况下,有必要考虑是否使用 Redux!
然而,现在有一个简单的解决方案可以将数据从一个组件传输到 React 树中更深层的其他组件:React Context。
使用 React Context 传递状态
该React.createContext
方法生成两个组件:
const {Provider, Consumer} = React.createContext(defaultValue);
- 负责
Provider
分发数据, Consumer
能够读取提供者数据。
让我们回到前三个组件。
// in App.js
import React, { Component } from 'react';
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
import styled from 'styled-components';
import { decode } from 'jsonwebtoken';
...
const UserContext = React.createContext({
user: null,
onLogout: () => true,
});
export const UserConsumer = UserContext.Consumer;
const UserProvider = UserContext.Provider;
export class App extends Component {
...
render() {
const { user } = this.state;
return (
<UserProvider
value={{
user,
onLogout: this.handleLogout,
}}
>
<Router>
<Container>
<Header />
<Switch>
<PrivateRoute
exact
path="/"
render={({ location }) => (
<Home location={location} />
)}
/>
...
// in PrivateRoute.js
import React from 'react';
import PropTypes from 'prop-types';
import { Redirect, Route } from 'react-router';
import { UserConsumer } from './App';
const PrivateRouteWithoutContext = ({ user, ...rest }) =>
user ? (
<Route {...rest} />
) : (
<Redirect
to={{
pathname: '/login',
state: { from: rest.location },
}}
/>
);
PrivateRouteWithoutContext.propTypes = {
user: PropTypes.object,
};
export const PrivateRoute = props => {
return (
<UserConsumer>
{({ user }) => (
<PrivateRouteWithoutContext user={user} {...props} />
)}
</UserConsumer>
);
};
请注意,Consumer
使用渲染道具模式。
// in components/Header.js
import React from 'react';
import PropTypes from 'prop-types';
import { UserConsumer } from '../App';
import { Navigation } from './Navigation';
export const HeaderWithoutContext = ({ user, onLogout }) => (
<header>
<h1>JavaScript Playground: meetups</h1>
{user && <Navigation onLogout={onLogout} />}
</header>
);
HeaderWithoutContext.propTypes = {
user: PropTypes.object,
onLogout: PropTypes.func.isRequired,
};
export const Header = () => {
return (
<UserConsumer>
{({ user, onLogout }) => (
<HeaderWithoutContext user={user} onLogout={onLogout} />
)}
</UserConsumer>
);
};
React Context 是一种将数据从应用程序的 N 级组件直接传送到任何 Nx 级子组件的简单方法。
那么,Redux 还是 Not Redux?
当项目达到一定复杂度时,Redux 就会变得有趣。然而,预先判断代码的复杂程度通常不是一个好主意!我更喜欢保持简单,事后对自己说:“太好了!我要开始做一些复杂的事情了。” 这让我想起了几年前,我系统地使用 Symfony 来启动一个 PHP 项目,而 Silex 让启动过程更加舒适和快捷。
尽管如此,就像 Symfony 一样,使用 Redux 可以成为非常明智的选择。
在项目初期使用它只是一个不成熟的决定。
这真的不是什么新鲜事😄
你可能不需要 Redux。
— 丹·阿布拉莫夫 ( @dan_abramov ) 2016 年 9 月 19 日
此外,除了这些理论上的考虑之外,脱离 Redux 似乎也会带来有益的影响。
首先,我更专注于 React!通过编写本文中的第二个示例,我重新发现了仅用组件积木构建应用程序的乐趣:就像玩乐高积木一样。使用render prop允许在整个项目中复用代码,同时保持 React 组件嵌套的逻辑。这是一种强大的模式,不像HOC那样神奇。此外,它还能适配 Redux 的可能实现。react -admin 2.0就是一个例证,它通过 render prop将UI 部分与应用程序逻辑分离。
最后,这似乎是 React 团队正在采取的方向。借助新的Context API,他们提供了无需采用 Redux 即可轻松设置全局共享存储的可能性。
鏂囩珷鏉ユ簮锛�https://dev.to/alexisjanvier/managing-state-in-react-redux-or-not-redux-4ebp