React Hooks和Redux哪个才是更好的状态管理策略?

译文
开发 前端
本文先介绍了如何使用Redux Toolkit的综合状态管理策略,去实现全局存储;然后探究了一个简单的应用程序,如何通过使用核心的React Hooks,去实现状态管理的各个细节;最后得出两者可以混合使用,相互补足的使用建议。

[[426178]]

【51CTO.com快译】如果您是一名React开发人员,那么一定对状态管理策略并不陌生。当我们在使用React去构建Web应用时,所有信息都被保存在所谓的状态之中。我们只需要更新该状态,即可实现对Web应用的更新。而状态管理,是指在应用程序的生命周期中,处理各种事件,并控制不同组件之间数据传递的过程。

一直以来,我们都习惯于使用针对JavaScript应用的、流行且强大的Redux库,作为状态容器。而React本身在其16.8版中已增加了Hooks。在本文中,我将根据自己在使用React SDK,构建生产级数据可视化工具过程中的经验,和您探讨这两种状态管理的方法,并介绍作为第三种方法的混合使用。

状态管理战略规划

首先,让我们来考虑状态管理的两个难题:需要存储什么状态,以及为什么要如此。毕竟,在数据可视化的应用中,并非所有状态都是相同的。

如下应用示例所示,我们希望通过图表中显示的节点和链接,以获悉当前的各个连接,以及与时间线组件共享的数据,进而甄别出数据集中的时间戳。其Sidebar包括了用于搜索和更新图表、及时间线的UI元素。简单而言,我们的目标就是实现如下图形和时间线的可视化。具体请参见--KronoGraph(。

在状态管理策略的规划阶段,我们可以通过在轴上绘制状态,以了解正在处理的具体内容:

如上图所示,我们在此所遵循的原则为:

  • 条目类型:除非您正在构建一个通用应用,否则图表和时间线中的节点类型(如:人员、地点、车辆)都应当尽可能是静态的。由于我们可以提前定义它们,因此它们不需要带有状态,可以位于存储库的配置文件中。
  • 条目样式:包含了每个节点和链接类型的核心样式,以及它们的预期逻辑。
  • 主题选择:为用户提供了在暗模式与亮模式之间切换的选项,并通过该状态的变化,去跟踪用户的偏好。
  • UI状态:UI状态包括静态和临时等。虽然我们没有必要在状态中,存储所有关于表单的交互,但是需谨防那些可能导致应用处于无响应状态的常见错误。
  • 条目位置和时间线范围:网络中的节点位置可能并不固定:
  1. 在图表中,用户可以根据偏好进行布局,并手动定位节点。
  2. 在时间线中,用户可以放大其感兴趣的时间范围。
  3. 在不同的会话中,通过位置的保持,用户可以从上一次中断处继续。
  • 撤消与重做栈:在高级应用中,我们需要通过设计,让用户能够在各自的当前会话中,保留撤消与重做数据的权限。
  • 来自API的数据:功能强大的应用程序,需要将那些从外部端点或API接收来的、动态且临时的数据缓存起来,并保存它们在应用中的状态。

状态管理的方法

有了前面状态管理的规划,我们来考虑应用中的数据层次结构。目前,我们有三种主要的状态管理方法可供选择:

  • 处理组件中的状态,并按需使用Hook在状态间进行传递。这种方法通常被称为“prop drilling”或“提升状态”,常被推荐用于基础类的应用。
  • 使用某种全局存储,以便所有组件都可访问。Redux之类的库可以提供该功能。
  • 使用混合方法,将Hook与那些经过慎重选择的重要状态相结合。

下面,让我们通过上述数据可视化的应用,来进一步探索这三种方法。

Redux状态管理

自2015年被发布以来,Redux已经成为了React生态系统的关键部分。它使用不变性(immutability)来简化应用程序的开发和逻辑设计。通过将处于某种状态的所有条目,强制设置为不变性,我们可以跟踪对于数据的任何更改,进而避免可能导致意外错误发生的数据突变。

虽然Redux目前仍是状态复杂的大型应用的绝佳选择,但是随着时间的推移,它变得日渐臃肿。为了协助降低其复杂性,Redux Toolkit于2019年应运而生,并成为了Redux的首推方式。

一致性的状态更新

Redux的一个核心概念是reducer。对于那些具有函数编程经验的人而言,这是一个能够接受多个输入,并将其减少为单个输出的函数。在状态管理中,该扩展能够让您通过采用一个或多个状态的更新指令,为图表生成一致性的状态更新。

让我们来考虑一个标准化的图形可视化用例:在图表中添加和删除节点。为了在全局存储中创建一个状态“切片”,我们在store.js中创建了如下代码:

JavaScript

import { configureStore } from '@reduxjs/toolkit'
import itemsReducer from '../features/chart/itemsSlice'
  
export const store = configureStore({ 
 reducer: { 
   items: itemsReducer 
 } 
}); 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.

为了让应用程序中的其他组件能够访问该存储,我们可以对应用程序进行如下“包装”:

JavaScript

importReactfrom 'react'
import ReactDOM from 'react-dom'
import './index.css'
import App from './App'
import { store } from './app/store'
import { Provider } from 'react-redux'
import * as serviceWorker from './serviceWorker'
  
ReactDOM.render( 
 <React.StrictMode> 
   <Provider store={store}> 
     <App /> 
   </Provider> 
 </React.StrictMode>, 
 document.getElementById('root'
); 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.

其中的Provider段意味着,其任何下游都可以访问该存储。在itemsSlice.js中,我们为各个条目定义了状态切片:

JavaScript

import { createSlice, createEntityAdapter } from '@reduxjs/toolkit'
  
export const itemsAdapter = createEntityAdapter(); 
const initialState = itemsAdapter.getInitialState(); 
  
export const itemsSlice = createSlice({ 
 name'items'
 initialState, 
 reducers: { 
   addItems: itemsAdapter.addMany, 
   addItem: itemsAdapter.addOne, 
   removeItems: itemsAdapter.removeMany, 
   removeItem: itemsAdapter.removeOne, 
 }, 
}); 
  
export const { addItems, addItem, removeItems, removeItem } = itemsSlice.actions; 
  
export const { select, selectAll, selectTotal } = itemsAdapter.getSelectors((state) => state.items); 
  
export default itemsSlice.reducer; 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.

通过上述代码段,我们可以获悉:

  • ReGraph的条目是各种通过ID索引的节点和链接对象。其核心数据结构十分常见。Redux Toolkit会通过一些辅助函数,来处理此类格式数据。在此,我们用到了由createEntityAdapter提供的addMany、addOne、removeMany、以及removeOne等功能。
  • 在Redux中,Selector允许我们从存储中获取一个状态片。我可以利用getSelectors适配器,来避免自行编写状态查询代码。
  • 最后,我们导出所有内容,以便在应用程序的其他地方使用。

在应用的其他代码中,我们还用到了store、reducer和selectors:

JavaScript

import React from 'react'
import { useSelector, useDispatch } from 'react-redux'
  
import { Chart } from 'regraph'
import { addItems, addItem, removeItems, removeItem, selectAll, selectTotal } from './itemsSlice'
  
import mapValues from 'lodash/mapValues'
  
import styles from './NetworkChart.module.css'
  
const colors = ['#173753''#6daedb''#2892d7''#1b4353''#1d70a2']; 
  
const defaultNodeStyle = (label) => ({ 
 label: { 
   text: `User ${label}`, 
   backgroundColor: 'transparent'
   color: 'white'
 }, 
 border: { width: 2, color: 'white' }, 
 color: colors[(label - 1) % colors.length], 
}); 
  
const styleItems = (items, theme) => { 
 return mapValues(items, (item) => { 
   if (item.id1) { 
     return { ...defaultLinkStyle(item.id), ...theme[item.type] }; 
   } else { 
     return { ...defaultNodeStyle(item.id), ...theme[item.type] }; 
   } 
 }); 
}; 
  
export function NetworkChart() { 
 const dispatch = useDispatch(); 
  
 const items = useSelector(selectAll); 
 const itemCount = useSelector(selectTotal); 
  
 const theme = { user: {} }; 
 const styledItems = styleItems(items, theme); 
  
 return ( 
   <div className={styles.container}> 
     <Chart 
       items={styledItems} 
       animation={{ animate: false }} 
       options={{ backgroundColor: 'rgba(0,0,0,0)', navigation: false, overview: false }} 
     /> 
     <div className={styles.row}> 
       <button 
         className={styles.button} 
         aria-label="Add items" 
         onClick={() => dispatch(addItem({ id: itemCount + 1, type: 'user' }))} 
       > 
         Add User 
       </button> 
       <button 
         className={styles.button} 
         aria-label="Remove Items" 
         onClick={() => dispatch(removeItem(itemCount))} 
       > 
         Remove User 
       </button> 
     </div> 
   </div> 
 ); 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.
  • 64.
  • 65.
  • 66.
  • 67.

通过使用Redux Hook的suseSelector,我们可以轻松利用切片代码,来提供选择器。同时,其useDispatch允许我们根据状态的“调度(dispatch)”动作(Redux的另一个实用部分),去变更状态。

使用Redux管理状态去添加和删除节点

Redux Toolkit使用时下流行的不变性库--Immer,对状态进行“纯净”地更新,而无需额外编写复杂的克隆和更新逻辑。在此,我们直接在组件中设置了图表项的样式。

当您从外部来源获取数据时,应用程序的状态和数据库的存储之间,很难被清晰地界定。与Redux Toolkit同源的RTK Query则通过与诸如react-query之类的库相配合,避免了从零开始编写缓存等功能。

如果您的应用单纯依赖Redux,那么可以将整个应用的状态放在全局存储中,以便每个组件都能访问它。当然,实际上只有某一些可视化组件的状态,需要通过Hooks和Redux的混合方法,实现存储。

Prop Drilling

著名的软件工程教育者--Kent C. Dodds曾提出了一个重要的观点:应保持状态尽可能地靠近需要的地方。对于上述示例,这意味着如果我们希望在图表和时间线组件之间共享数据,则可以通过Prop Drilling来简化并实现。这将是一种跨组件共享状态的有效且纯净的方式。也就是说,如果我们将状态带到VisualizationContainer应用中,则可以将数据作为prop传递到每个组件处。当然,如果我需要在复杂的层次结构中上下传递,则仍可以使用Redux。

凭借着其强大的API和一些精心设计的prop,ReGraph在控制其内部状态方面,非常有效。我们甚至不需要让过多的prop流转到图表的组件之外。

React Hooks

就示例中的图表组件而言,我们可以使用simpleuseState和useRefHooks,来处理状态中的基本配置。ReGraph可以将那些对于状态的多次更新处理,通过单独调用useState对方式来实现,进而免去了prop组的频繁更新。

JavaScript

const [layout, setLayout] = useState(defaults.layout); 
setLayout({name'sequential'}) 
  • 1.
  • 2.

对于使用过Redux的人来说,Hook的useReducer以及如下代码段,一定不会陌生。

JavaScript

import React, { useState, useReducer, useCallback } from 'react'
  
const [combine, combineDispatch] = useReducer(combineReducer, defaults.combine) 
  const combineItems = useCallback(property => combineDispatch({ type: 'COMBINE', property }), []) 
  const uncombineItems = useCallback(property => combineDispatch({ type: 'UNCOMBINE', property }), []) 
  
  
function combineReducer(combine, action) { 
  const newCombine = { ...combine }; 
  if (action.type === 'COMBINE') { 
    newCombine.properties.push(action.property); 
    newCombine.level = combine.level + 1; 
  } 
  else if (action.type === 'UNCOMBINE') { 
    newCombine.properties.pop(); 
    newCombine.level = combine.level - 1; 
  } else { 
    throw new Error(`No action ${action.type} found`); 
  } 
  return newCombine; 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.

值得注意的是,没有了Redux Toolkit的帮助,我们需要人工更新已组合的对象。这就意味着,更多的代码需要被编写。在上述ReGraph之类的小型应用示例中,我们手动编写了reducer。

React的useReducer与Redux中的reducer之间存在概念上的差异。在React中,我们编写了任意数量的reducer。它们只是各种便于更新状态的Hooks。而在Redux中,它们作为概念性的分离,以应对集中式的存储。

正如下面代码段所示,我们可以为ReGraph编写一个定制的Hook,来封装所有需要用到的prop:

JavaScript

import React, { useState, useReducer, useCallback } from 'react'
  
import { has, merge, mapValues, isEmpty } from 'lodash'
import { chart as defaults } from 'defaults'
  
const linkColor = '#fff9c4'
const nodeColor = '#FF6D66'
  
function isNode(item) { 
  return item.id1 == null && item.id2 == null

  
function transformItems(items, itemFn) { 
  return mapValues(items, (item, id) => { 
    const newItem = itemFn(item, id); 
    return newItem ? merge({}, item, newItem) : item 
  }); 
}; 
  
function styleItems(items) { 
  return transformItems(items, item => { 
    return defaults.styles[isNode(item) ? 'node' : 'link']; 
  }); 

  
  
function itemsReducer(items, action) { 
  const newItems = { ...items }; 
  if (action.type === 'SET') { 
    return { ...newItems, ...styleItems(action.newItems) } 
  } 
  else if (action.type === 'REMOVE') { 
    Object.keys(action.removeItems).forEach(removeId => { delete newItems[removeId]; }) 
    return newItems; 
  } else { 
    throw new Error(`No action ${action.type} found`); 
  } 

  
function combineReducer(combine, action) { 
  const newCombine = { ...combine }; 
  if (action.type === 'COMBINE') { 
    newCombine.properties.push(action.property); 
    newCombine.level = combine.level + 1; 
  } 
  else if (action.type === 'UNCOMBINE') { 
    newCombine.properties.pop(); 
    newCombine.level = combine.level - 1; 
  } else { 
    throw new Error(`No action ${action.type} found`); 
  } 
  return newCombine; 

  
function useChart({ initialItems = {} }) { 
  
  const styledItems = styleItems(initialItems) 
  
  const [items, dispatch] = useReducer(itemsReducer, styledItems) 
  const addItems = useCallback(newItems => dispatch({ type: 'SET', newItems }), []) 
  const removeItems = useCallback(removeItems => dispatch({ type: 'REMOVE', removeItems }), []) 
  
  const [combine, combineDispatch] = useReducer(combineReducer, defaults.combine) 
  const combineItems = useCallback(property => combineDispatch({ type: 'COMBINE', property }), []) 
  const uncombineItems = useCallback(property => combineDispatch({ type: 'UNCOMBINE', property }), []) 
  
  const [animation, setAnimation] = useState(defaults.animation); 
  const [view, setView] = useState(defaults.view); 
  
  const [layout, setLayout] = useState(defaults.layout); 
  const [positions, setPositions] = useState(defaults.positions); 
  const [selection, setSelection] = useState(defaults.selection); 
  const [map, setMap] = useState(defaults.map); 
  
  const [options, setOptions] = useState(defaults.options); 
  
  const chartState = { items, options, layout, positions, selection, map, animation, combine } 
  return [chartState, { addItems, removeItems, setPositions, setSelection, combineItems, uncombineItems }] 

  
export { useChart, isNode } 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.
  • 64.
  • 65.
  • 66.
  • 67.
  • 68.
  • 69.
  • 70.
  • 71.
  • 72.
  • 73.
  • 74.
  • 75.
  • 76.
  • 77.
  • 78.
  • 79.
  • 80.
  • 81.

值得注意的是,由于ReGraph会针对每一个prop用到大量的useState调用,因此我们可以将它们放入一个简单的对象中,并通过单个函数处理,来实现更新。为了简单起见,我们可以使用lodash merge,来合并条目的更新。同时,在生产环境中,我们会使用Immer之类的工具,来提高更新的效率。

Context API

我们定制的useChart Hook足以满足让单个组件去控制图表。但是,我们又该如何处置Sidebar呢?此时,我们就需要通过全局范围的Redux来解决。

作为React API的一部分,由于Context可以让各种数据在用户定义的范围内,被访问到,因此它可以协助我们实现在Redux中,创建全局存储。

虽然,业界有对于context是否能成为Redux useContext替代品的争论,但是有一点可以肯定:它是一种纯净的API,可以在组件之间一致性地共享context。 如下代码段展示了如何使用Hook和Context:

JavaScript

import React, { useState, useReducer, useCallback } from 'react'
  
import merge from 'lodash/merge'
import mapValues from 'lodash/mapValues'
  
import { chart as defaults } from 'defaults'
  
const ChartContext = React.createContext(); 
  
function isNode(item) { 
 return item.id1 == null && item.id2 == null

  
function transformItems(items, itemFn) { 
 return mapValues(items, (item, id) => { 
   const newItem = itemFn(item, id); 
   return newItem ? merge({}, item, newItem) : item; 
 }); 

  
function styleItems(items) { 
 return transformItems(items, (item) => { 
   return defaults.styles[isNode(item) ? 'node' : 'link']; 
 }); 

  
function itemsReducer(items, action) { 
 const newItems = { ...items }; 
 if (action.type === 'SET') { 
   return { ...newItems, ...styleItems(action.newItems) }; 
 } else if (action.type === 'REMOVE') { 
   Object.keys(action.removeItems).forEach((removeId) => { 
     delete newItems[removeId]; 
   }); 
   return newItems; 
 } else { 
   throw new Error(`No action ${action.type} found`); 
 } 

  
function combineReducer(combine, action) { 
 const newCombine = { ...combine }; 
 if (action.type === 'COMBINE') { 
   newCombine.properties.push(action.property); 
   newCombine.level = combine.level + 1; 
 } else if (action.type === 'UNCOMBINE') { 
   newCombine.properties.pop(); 
   newCombine.level = combine.level - 1; 
 } else { 
   throw new Error(`No action ${action.type} found`); 
 } 
 return newCombine; 

  
function ChartProvider({ children }) { 
 const [items, dispatch] = useReducer(itemsReducer, {}); 
 const addItems = useCallback((newItems) => dispatch({ type: 'SET', newItems }), []); 
 const removeItems = useCallback((removeItems) => dispatch({ type: 'REMOVE', removeItems }), []); 
  
 const [combine, combineDispatch] = useReducer(combineReducer, defaults.combine); 
 const combineItems = useCallback((property) => combineDispatch({ type: 'COMBINE', property }),[]); 
 const uncombineItems = useCallback((property) => combineDispatch({ type: 'UNCOMBINE', property }),[]); 
  
 const [animation, setAnimation] = useState(defaults.animation); 
 const [view, setView] = useState(defaults.view); 
  
 const [layout, setLayout] = useState(defaults.layout); 
 const [positions, setPositions] = useState(defaults.positions); 
 const [selection, setSelection] = useState(defaults.selection); 
 const [map, setMap] = useState(defaults.map); 
  
 const [options, setOptions] = useState(defaults.options); 
  
  
 const value = [ 
   { view, items, options, layout, positions, selection, map, animation, combine }, 
   { addItems, removeItems, setOptions, setMap, setView, setLayout, setAnimation, setPositions, setSelection, combineItems, uncombineItems }, 
 ]; 
  
 return <ChartContext.Provider value={value}>{children}</ChartContext.Provider>; 

  
function useChart() { 
 const context = React.useContext(ChartContext); 
 if (context === undefined) { 
   throw new Error('useChart must be used within a ChartProvider'); 
 } 
 return context; 

  
export { ChartProvider, useChart }; 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.
  • 64.
  • 65.
  • 66.
  • 67.
  • 68.
  • 69.
  • 70.
  • 71.
  • 72.
  • 73.
  • 74.
  • 75.
  • 76.
  • 77.
  • 78.
  • 79.
  • 80.
  • 81.
  • 82.
  • 83.
  • 84.
  • 85.
  • 86.
  • 87.
  • 88.
  • 89.
  • 90.
  • 91.

下面,我使用定制的ChartProvider上下文,来包装那些需要访问图表的详细信息,以及设置器的任何组件:

HTML

<App> 
    <ChartProvider> 
        <VisualizationContainer> 
            <Chart/> 
            <Timeline/> 
        </VisualizationContainer> 
        <Sidebar/> 
    </ChartProvider> 
</App> 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.

接着,我们需要通过如下简单的调用,导入useChart,并获取当前图表的状态,以及应用层次结构中任意位置的调度函数。

const  [state, { setLayout }] = useChart(); 
  • 1.

Context与Redux

可见,使用Context和Redux存储之间的关键区别在于,Context必须由您来定义范围,而不会自动地为应用程序的其余部分提供服务。这会迫使我们更加有意识地去规划应用程序的逻辑。正如useReducer那样,我们通常的做法是:创建许多不同的上下文,以供应用程序去使用。

小结

综上所述,我们先介绍了如何使用Redux Toolkit的综合状态管理策略,去实现全局存储;然后探究了一个简单的应用程序,如何通过使用核心的React Hooks,去实现状态管理的各个细节;最后得出两者可以混合使用,相互补足的使用建议。

原文标题:React Hooks vs. Redux: Choosing the Right State Management Strategy ,作者:Christian Miles

【51CTO译稿,合作站点转载请注明原文译者和出处为51CTO.com】

 

责任编辑:华轩 来源: 51CTO
相关推荐

2021-08-14 08:45:27

React开发应用程序

2024-04-22 09:12:39

Redux开源React

2021-11-05 10:36:19

性能优化实践

2021-07-26 09:00:08

ReactHooks 项目

2019-03-13 10:10:26

React组件前端

2024-04-25 09:10:50

ReactReduxJavaScript

2019-08-20 15:16:26

Reacthooks前端

2022-03-18 14:09:52

ReactJavaScript

2023-11-06 08:00:00

ReactJavaScript开发

2024-01-08 09:36:47

管理库代码

2020-10-28 09:12:48

React架构Hooks

2021-12-17 19:15:51

前端虫洞状态

2022-04-21 08:01:34

React框架action

2021-03-18 08:00:55

组件Hooks React

2017-09-08 13:35:48

云优先策略互联网

2017-03-02 14:52:46

2021-06-28 11:17:14

CoutPrintf接口

2020-09-19 17:46:20

React Hooks开发函数

2022-03-31 17:54:29

ReactHooks前端

2024-02-05 21:48:25

VueReactHooks
点赞
收藏

51CTO技术栈公众号