React 生态中 react-router 有大版本更新,redux 更是推出一个 wrapper 包 - react-toolkit,算是总结过去,优化最佳实践。
react-router
react-router-dom 更新到了 v6 版本,从之前的版本中,重新总结了最佳特性。
- v6 版本依赖 React hooks 特性,因此需要 React 16.8 及以上
- 自带类型声明,不再需要 @types/react-router-dom
- Switch Comp => Routes Comp
- 路由匹配基于最佳匹配,而不是顺序
- Route 组件
- 移除 exact prop
- component prop => element prop,element 更加便于给组件传参
- 因为嵌套路由保留了 children 属性,所以只能新增 element 属性
- 简化路由格式,仅支持
:params
和*
通配符 - 可选参数:
task?/:taskId
- 新增 Outlet 组件为嵌套服务,用于占位
- useOutletContext 用于给父路由给子路由共享状态
- useHistory => useNavigate
- Redirect Comp => Navigate Comp
- 新增 useRoutes hooks 用于通过对象的方式定义路由,Routes 本质上就是 useRoutes 的 wrapper
- 新增 useSearchParams 用于解析查询参数
- 移除 Link 组件的 component 属性
- NavLink 组件
- exact props rename to end
- 移除 activeClassName 和 activeStyle 属性,而是扩展 className prop 支持函数形式,回调参数是 isActive 值
- useRouteMatch to useMatch
react-router 基础
目的:更好的进行代码逻辑拆分。
Router 核心概念
- Location State:未编码在 URL 中,而是无形地储存在浏览器的内存中,常用场景
- 告诉下一页用户来自哪里,并分发 UI
- 将列表中的部分记录发送到下一个屏幕,以便它可以立即呈现部分数据,然后在之后获取其余数据。
- 可使用 useLocation().state 得到数据
- Nested Routes 嵌套路由
- Relative Links:不是使用
/
开头的路径表示相对路径,将会继承最近的路由 - Index Route:没有路径的子路由,在父路由的 outlet 中呈现。可理解成默认子路由
- Layout Route:布局路由,没有路径的父路由,专门用于在特定布局中分组子路由。用于减少重复的布局组件
基础 API
- 相关组件
- Route 组件类似于 if 语句,element 属性默认值是 Outlet,意味着你可以不用显式声明 element
- Outlet 可以放在任意地方,以适应你的布局
- 相关 hooks:useParams、useLocation、useSearchParams、useMatch、useNavigate、useOutletContext
- useMatch 通过给定的路径匹配串和当前的 location 进行解析
- useLocation 当你需要知道 location 变化时很有用
- useOutletContext 父路由与子路由分享状态
- 嵌套路由:布局和 URL 片段耦合,通过 Outlet 承载子组件的渲染
- 支持使用多个 Routes 组件,且支持嵌套
- 可缺省路由设计:某个参数可有可无,v6 版本移除了正则匹配规则,因此你可以通过定义多个 Route 解决
默认子路由、空路由、Modal 路由
- 默认子路由:通过 index 属性
- 空路由:通过指定 path 为
*
即可,是最弱的匹配规则 - Modal 路由:有趣的魔法,需要时可以考虑
进行代码设计时思考几个选型
- 平级路由 OR 嵌套路由
- 路由参数 OR 查询参数
- 父子路由状态共享
路由设计实例
<Routes>
<Route path="/" element={<App />}>
<Route index element={<Home />} />
<Route path="teams" element={<Teams />}>
<Route path=":teamId" element={<Team />} />
<Routes path=":teamId/edit" element={<EditTeam />} />
<Route path="new" element={<NewTeamForm />} />
<Route index element={<LeagueStandings />} />
</Route>
</Route>
<Route element={<PageLayout />}>
<Route path="/privacy" element={<Privacy />} />
<Route path="/tos" element={<Tos />} />
</Route>
</Routes>
两种常用 state 场景
<Link to="/pins/123" state={{ fromDashboard: true }} />;
let navigate = useNavigate();
navigate("/users/123", { state: partialUser });
有点取巧的设计:实现特点路由才出现的组件,这样可以突破仅通过 outlet 布局的限制
function MatchPath({ path, Comp }) {
let match = useMatch(path);
return match ? <Comp {...match} /> : null;
}
// Will match anywhere w/o needing to be in a `<Routes>`
<MatchPath path="/accounts/:id" Comp={Account} />;
auth
auth 场景示例
function AuthProvider({ children }: { children: React.ReactNode }) {
let [user, setUser] = React.useState<any>(null);
let signin = (newUser: string, callback: VoidFunction) => {
return fakeAuthProvider.signin(() => {
setUser(newUser);
callback();
});
};
let signout = (callback: VoidFunction) => {
return fakeAuthProvider.signout(() => {
setUser(null);
callback();
});
};
let value = { user, signin, signout };
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}
function RequireAuth({ children }: { children: JSX.Element }) {
let auth = useAuth();
let location = useLocation();
if (!auth.user) {
// Redirect them to the /login page, but save the current location they were
// trying to go to when they were redirected. This allows us to send them
// along to that page after they login, which is a nicer user experience
// than dropping them off on the home page.
return <Navigate to="/login" state={{ from: location }} replace />;
}
return children;
}
function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault();
auth.signin(username, () => {
// Send them back to the page they tried to visit when they were
// redirected to the login page. Use { replace: true } so we don't create
// another entry in the history stack for the login page. This means that
// when they get to the protected page and click the back button, they
// won't end up back on the login page, which is also really nice for the
// user experience.
navigate(from, { replace: true });
});
}
modal route
模态框路由示例
function App() {
let location = useLocation();
// The `backgroundLocation` state is the location that we were at when one of
// the gallery links was clicked. If it's there, use it as the location for
// the <Routes> so we show the gallery in the background, behind the modal.
let state = location.state as { backgroundLocation?: Location };
return (
<div>
<h1>Modal Example</h1>
<Routes location={state?.backgroundLocation || location}>
<Route path="/" element={<Layout />}>
<Route index element={<Home />} />
<Route path="gallery" element={<Gallery />} />
<Route path="/img/:id" element={<ImageView />} />
<Route path="*" element={<NoMatch />} />
</Route>
</Routes>
{/* Show the modal when a `backgroundLocation` is set */}
{state?.backgroundLocation && (
<Routes>
<Route path="/img/:id" element={<Modal />} />
</Routes>
)}
</div>
);
}
function Gallery() {
let location = useLocation();
return (
<div style={{ padding: "0 24px" }}>
<h2>Gallery</h2>
<div
style={{
display: "grid",
gridTemplateColumns: "repeat(auto-fit, minmax(200px, 1fr))",
gap: "24px",
}}
>
{IMAGES.map((image) => (
<Link
key={image.id}
to={`/img/${image.id}`}
// This is the trick! Set the `backgroundLocation` in location state
// so that when we open the modal we still see the current page in
// the background.
state={{ backgroundLocation: location }}
>
<img
width={200}
height={200}
style={{
width: "100%",
aspectRatio: "1 / 1",
height: "auto",
borderRadius: "8px",
}}
src={image.src}
alt={image.title}
/>
</Link>
))}
</div>
</div>
);
}
redux-toolkit
redux-toolkit 二次封装了 redux 核心相关 api,如 createStore + combineReducers + applyMiddleware + compose 的使用,旨在标准化 redux 书写逻辑,简化配置,减少样板代码。
从哪些场景对 redux 进行了优化
- store 初始化:configureStore 自动组合 reducer、装配 redux-thunk 插件、激活 Redux DevTools Extension
- reducer 定义:immutable 更新、逃离 switch-case
- slice 切片式的单文件内聚的状态定义
- 集成一些场景的插件如:redux-thunk、reselect
- 更好的 typescript 支持,手动支持 typescript 是一件很繁琐的事情
基础 api
- createReducer:优化写法、内置 immer 方便编写不可变更新
- createAction:泛型的方式让 payload 具有类型
许多不同的切片可能希望通过清除数据或重新设置到初始状态值来响应“用户登出”操作。
文档中建议你使用 RTK Query 处理,可以消除编写任何 thunk 或 reducer 来管理数据获取的需要。如果你需要自己写数据获取逻辑,推荐使用 redux-thunk 中间件用作标准方法,因为它足以满足大多数典型用例,这在 RTK 中已默认集成。
三个核心 api
- createSlice
- slice
- initialState
- reducers
- extraReducers
- createAsyncThunk
- 优化了对于请求 started、succeeded、failed 的标准处理逻辑
- createEntityAdapter 管理规范化数据
normalizr
是一个流行的用于规范化数据的现有库- 相关配置
- selectId
- sortComparer
- 内置了针对
{ ids: [], entities: {} }
结构的常见 CRUD 操作 - setAll、addMany、upsertMany、getSelectors
其他
- createSelector:来源于 reselect 库
- createListenerMiddleware
- query module
可选择使用 redux-persist 进行数据持久化
query
作为 @reduxjs/toolkit
附加插件的形式,服务于解决数据获取与缓存场景。主要有数据获取、加载状态、结果缓存、自动生成 react-hooks 等功能。灵感起源于 React Query
、SWR
、Apollo
和 Urql
。
简单了解下相关 api
- 建立在 createSlice + createAsyncThunk 之上
- createApi:有点类似于 createSlice 的用法,用于自动生成 hooks、reducer、middleware
- reducerPath
- baseQuery:调用 fetchBaseQuery 生成
- endpoints 定义相关获取数据的 api 函数
- injectEndpoints 帮助你从其他模块获取 endpoints,便于模块拆分
- useXXX 如下相关逻辑
- data、status、error
- isLoading、isFetching、isSuccess、isError
- 上面的功能实现,每个请求都手动实现的话,是很繁琐的一件事
- 高级特性
- 订阅相同查询的任何组件将始终使用相同的数据
- 自动删除重复的请求,这样您就不必担心检查正在运行的请求和性能优化
react-redux
react-redux 发布了 v8 版本
- 支持了 React 18 且向下兼容,API 保持不变,仅内部优化
- 使用 TypeScript 重写,不再需要 @types/react-redux 包
react-redux 注意点
- useSelector(selector: Function, equalityFn?: Function)
- 使用 useSelector 时,如果你返回的是自己二次封装的对象,注意使用第二参数,否则其他状态更新,由于比较结果,使用的是
===
比较,不同于 connect 的 shallow compare,因此会导致组件无意义的 re-render。对此 react-redux 直接提供了 shallowEqual 函数供使用 - reselect 库解决选择时性能问题
State Management
判断是否需要 Redux 简单方式是:在本地管理状态开始显得混乱,随着应用程序的增长,跨组件的状态共享也变得单调乏味。
使用 Redux 除了解决跨组件通信的问题外,更重要的是逻辑拆分问题。
最重要的是组件设计和模块化划分要做好,这样一来大部分情况,如果组件简单,很好理解,自然不需要引入其他解决方案。
Recoil 是新出的状态管理工具:非常的简单易用易学(Atoms and Selectors),就像是全局的 useState