在 React Native 中使用 context 进行状态管理
使用全局钩子
我相信很多 React 和 React Native 开发者都熟悉使用 Redux 来管理应用状态。几个月前,我写了一篇文章,介绍了如何在 React 中使用 Context 代替 Redux 来管理全局状态。将状态尽可能地放在需要它的地方是一种很好的实践,而这在 React 中很容易实现,因为React Router的 API 非常简单。另一方面,由于React Navigation的 API 相当复杂,这种做法在 React Native 中可能会遇到一些困难。虽然 React Native 中还有其他导航库可供选择,例如 React Router Native,但 React Navigation 似乎是 React Native 中最常用的导航库。因此,这里介绍一种开发者在 React Native 中构建 Context 提供程序的方法:
// placing all providers in the app's root
<AuthContext.provider value={authValue}>
<ArticleContext.provider value={articleValue}>
<UserContext.provider value={userValue}>
<Navigator />
</UserContext.provider>
</ArticleContext.provider>
</AuthContext.provider>
假设 Navigator 是导航组件,负责路由到 App 中的所有其他组件,那么像上面那样设置上下文提供程序可能会对 App 的性能产生负面影响,因为这意味着当任何提供程序的值发生变化时,整个 App 都会重新渲染,包括那些不需要或不使用此次更新的组件。在本文中,我将展示一种非常简洁的方法来设置导航和上下文,以便组件仅在需要更新的提供程序下进行渲染。
在我们的示例应用中,我们将包含用户上下文、文章上下文和身份验证上下文。稍后我会详细介绍文章组件,展示我们如何使用上下文。
创建语境
我们将首先创建各种上下文。我并不喜欢直接使用提供者,而是喜欢将它们抽象到我称之为“控制器”的其他组件中。这样可以轻松地隔离和修改创建和更新上下文值的逻辑。控制器会返回我们的提供者。
这是我们的身份验证上下文内容:
import React, { useReducer, useMemo } from 'react';
import PropTypes from 'prop-types';
const initialState = {
loggedIn: false,
user: {}
};
const initialContext = [{ ...initialState }, () => {}];
export const AuthContext = React.createContext(initialContext);
const updater = (state, update) => {
return { ...state, ...update };
};
export function AuthController(props) {
const [authState, updateAuth] = useReducer(updater, initialState);
const value = useMemo(() => [authState, updateAuth], [authState]);
return (<AuthContext.Provider value={value}>
{props.children}
</AuthContext.Provider>);
}
AuthController.propTypes = {
children: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node
]).isRequired
};
关于用户上下文,我们有:
import React, { useReducer, useMemo } from 'react';
import PropTypes from 'prop-types';
const initialState = {
user: {}
};
const initialContext = [{ ...initialState }, () => {}];
export const UserContext = React.createContext(initialContext);
const updater = (state, update) => {
return { ...state, ...update };
};
export function UserController(props) {
const [userState, updateUser] = useReducer(updater, initialState);
const value = useMemo(() => [userState, updateUser], [userState]);
return (<UserContext.Provider value={value}>
{props.children}
</UserContext.Provider>);
}
UserController.propTypes = {
children: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node
]).isRequired
};
最后,文章背景:
import React, { useReducer, useMemo } from 'react';
import PropTypes from 'prop-types';
const initialState = {
articles: []
};
const initialContext = [{ ...initialState }, () => {}];
export const ArticleContext = React.createContext(initialContext);
const reducer = (state, action) => {
switch (action.type) {
case "get":
return {...state, articles: action.articles }
case "add":
return { ...state, articles: [...state.articles, action.article] };
case "delete":
const articles = [...state.articles];
const filteredArticles = articles.filter(article => article.id !== action.articleId);
return { ...state, articles:filteredArticles };
default:
throw new Error("Unrecognized action");
}
};
export function ArticleController(props) {
const [articleState, dispatch] = useReducer(reducer, initialState);
const value = useMemo(() => [articleState, dispatch], [articleState]);
return (<ArticleContext.Provider value={value}>
{props.children}
</ArticleContext.Provider>);
}
ArticleController.propTypes = {
children: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node
]).isRequired
};
这就是我们所有的上下文。我们将一个包含两个元素的数组作为值传递给上下文提供程序。数组中的第一个元素是我们的状态,第二个元素是一个用于更新状态的函数。这个值必须进行记忆化,以防止每次组件渲染时该值都会获得新的引用,从而导致持续的重新渲染。
分离导航和上下文提供者
首先,我们将创建主导航。请确保已安装 react-navigation。
npm i react-navigation
我们将定义我们的主导航器,它是子导航器的组合。
import { createStackNavigator, createAppContainer } from 'react-navigation';
import User from './user/';
import Article from './articles/';
const Navigator = createStackNavigator(
{
user: {
screen: User
},
article: {
screen: Article
}
},
{
initialRouteName: 'article'
}
);
export default createAppContainer(Navigator);
然后我们为与用户个人资料相关的组件创建一个子导航器。
import React from 'react';
import { createStackNavigator } from 'react-navigation';
import PropTypes from 'prop-types'
import UserDetails from './user-details.js';
import EditUser from './edit-user.js';
import UserController from '../contexts/user-context.js'
const UserNavigator = createStackNavigator({
userDetails: {
screen: UserDetails
},
editUser: {
screen: Edituser
}
},
{
initialRouteName: 'userDetails',
});
export default function User(props) {
return (
<UserController>
<UserNavigator navigation={props.navigation} />
</UserController>
);
}
User.router = UserNavigator.router
User.propTypes = {
navigation: PropTypes.object
};
类似地,文章相关组件的子导航器也存在。
import React from 'react';
import PropTypes from 'prop-types'
import { createStackNavigator } from 'react-navigation';
import ListArticles from './all-articles.js';
import AddArticle from './add-article.js';
import ArticlesController from '../contexts/article-context.js'
const ArticleNavigator = createStackNavigator({
listArticles: {
screen: ListArticles
},
addArticle: {
screen: AddArticle
}
},
{
initialRouteName: 'articleDetails',
});
export default function Article(props) {
return (
<ArtileController>
<ArticleNavigator navigation={props.navigation} />
</ArticleController>
);
}
Article.router = ArticleNavigator.router
Article.propTypes = {
navigation: PropTypes.object
};
目前我们已经将导航器拆分,以便将每个导航器封装在各自的提供程序中。控制器负责渲染这些提供程序。那么我们的身份验证上下文呢?由于身份验证可能涉及整个应用程序,我们可以将整个导航器封装在身份验证上下文中,这样每个组件都可以访问身份验证状态。
import React from 'react';
import Navigator from './navigator';
import { AuthController } from './context/auth-context';
export default function App() {
return (
<AuthController>
<Navigator />
</AuthController>
);
}
我们没有将所有路径都放在主导航器中,而是将它们拆分成多个子导航器,并将它们作为各自提供程序的子元素进行渲染,同时也会在主导航器中导入这些子导航器。要了解更多关于 React Native 导航的信息,您可以查看React 导航文档。
消费语境
下一步,我们使用上下文信息。在 ListArticles 组件中,我们是这样使用文章上下文信息的。
import React, {useEffect, useContext} from 'react';
import {Text, FlatList, ScrollView, TouchableOpacity} from 'react-native';
import PropTypes from 'prop-types';
import {getArticles, removeAricleFromDatabase} from 'api';
import {ArticleContext} from './context/article-context';
export default function ListArticles (props) {
const [articles, dispatch] = useContext(ArticleContext);
useEffect(() => {
getArticles()
.then(articles => dispatch({type:'get', articles})
}, []);
const deleteArticle = (article) => {
removeArticleFromDatabase(article)
.then((data) => dispatch({type: 'delete', articleId: data.id}));
const Item = ({id, title}) => {
return (
<View>
<Text>{item.title}</Text>
<TouchableOpacity onPress={(id) => deleteArticle(id)}>
<Text>x</Text>
</TouchableOpacity>
</View>
)
}
return (
<ScrollView>
<FlatList
data={articles}
renderItem={({item}) => <Item title={item.title} id={item.id}/>}
keyExtractor={item => item.id}
/>
<TouchableOpacity
onPress={() => props.navigation.navigate('addArticle')}>
<Text>Add new article</Text>
</TouchableOpacity>
</ScrollView>
);
}
这里我们使用 React 的useContext hook来获取文章上下文。我们将上下文作为参数传递给 hook,它会返回传递给 provider 的值。分发我们需要执行的操作会更新上下文 provider 的值。如果 provider 不存在于组件树层级结构中,我们将无法获取到值。
同样,我们可以派发一个操作来添加文章。
import React, {useContext} from 'react';
import {ArticleContext} from './context/article-context';
import {saveArticleInDatabase } from 'api';
const [_, dispatch] = useContext(ArticleContext);
const addArticle = (article) => {
saveArticleInDatabase(article)
.then((data) => dispatch({type: 'add', article: data}));
}
/* render beautiful jsx */
我们应用程序中的所有其他上下文都可以以相同的方式使用,每个上下文提供程序都只作为使用它的组件的父组件,以防止不必要的重新渲染。
这里介绍的模式并非一成不变。这只是一个指南,旨在帮助我们更好地利用 Context 来管理 React Native 应用的状态。要了解更多关于 React Context 的信息,请参阅 React 官方文档。
文章来源:https://dev.to/emeka/state-management-in-react-native-using-context-14no