我已经在底部进行了更新
是否可以通过多个Context API使用者维护自己的Provider值的整体根状态(如Redux),而不会在每个孤立的更改上触发重新呈现?
已经read through this related question并尝试了一些变体来测试那里提供的一些见解,但我仍然对如何避免重新渲染感到困惑。
完整的代码在下面,并且在此处在线:https://codesandbox.io/s/504qzw02nl
问题是,根据devtools的说法,即使SectionB
是唯一看到任何渲染更改的组件,即使b
是唯一的组件,每个组件也会看到“更新”(重新渲染)状态树中唯一更改的部分。我已经尝试过使用功能组件和PureComponent
进行此操作,并看到相同的渲染异常。
因为没有任何东西作为道具传递(在组件级别),所以我看不到如何检测或防止这种情况。在这种情况下,我将整个应用程序状态传递给提供程序,但我也尝试传递状态树的片段,并看到相同的问题。显然,我做错了什么。
import React, { Component, createContext } from 'react';
const defaultState = {
a: { x: 1, y: 2, z: 3 },
b: { x: 4, y: 5, z: 6 },
incrementBX: () => { }
};
let Context = createContext(defaultState);
class App extends Component {
constructor(...args) {
super(...args);
this.state = {
...defaultState,
incrementBX: this.incrementBX.bind(this)
}
}
incrementBX() {
let { b } = this.state;
let newB = { ...b, x: b.x + 1 };
this.setState({ b: newB });
}
render() {
return (
<Context.Provider value={this.state}>
<SectionA />
<SectionB />
<SectionC />
</Context.Provider>
);
}
}
export default App;
class SectionA extends Component {
render() {
return (<Context.Consumer>{
({ a }) => <div>{a.x}</div>
}</Context.Consumer>);
}
}
class SectionB extends Component {
render() {
return (<Context.Consumer>{
({ b }) => <div>{b.x}</div>
}</Context.Consumer>);
}
}
class SectionC extends Component {
render() {
return (<Context.Consumer>{
({ incrementBX }) => <button onClick={incrementBX}>Increment a x</button>
}</Context.Consumer>);
}
}
编辑:我了解可能有a bug in the way react-devtools条检测到或显示了重新渲染。我以显示问题的方式expanded on my code above。现在,我无法确定我在做什么实际上引起了重新渲染。根据我从Dan Abramov那里读到的内容,我认为我正在正确使用提供者和消费者,但是我不能确切地说出这是否正确。我欢迎有任何见识。
答案 0 :(得分:1)
据我了解,上下文API并非旨在避免重新渲染,而更像是Redux。如果您希望避免重新渲染,则可以考虑使用PureComponent
或生命周期挂钩shouldComponentUpdate
。
这是提高性能的好方法link,您也可以将其应用于上下文API
答案 1 :(得分:1)
我做了一个关于如何从 React.Context
中受益的概念证明,但避免重新渲染使用上下文对象的子项。该解决方案使用了 React.useRef
和 CustomEvent
。每当您更改 count
或 lang
时,只会更新使用特定属性的组件。
在下面查看,或尝试CodeSandbox
index.tsx
import * as React from 'react'
import {render} from 'react-dom'
import {CountProvider, useDispatch, useState} from './count-context'
function useConsume(prop: 'lang' | 'count') {
const contextState = useState()
const [state, setState] = React.useState(contextState[prop])
const listener = (e: CustomEvent) => {
if (e.detail && prop in e.detail) {
setState(e.detail[prop])
}
}
React.useEffect(() => {
document.addEventListener('update', listener)
return () => {
document.removeEventListener('update', listener)
}
}, [state])
return state
}
function CountDisplay() {
const count = useConsume('count')
console.log('CountDisplay()', count)
return (
<div>
{`The current count is ${count}`}
<br />
</div>
)
}
function LangDisplay() {
const lang = useConsume('lang')
console.log('LangDisplay()', lang)
return <div>{`The lang count is ${lang}`}</div>
}
function Counter() {
const dispatch = useDispatch()
return (
<button onClick={() => dispatch({type: 'increment'})}>
Increment count
</button>
)
}
function ChangeLang() {
const dispatch = useDispatch()
return <button onClick={() => dispatch({type: 'switch'})}>Switch</button>
}
function App() {
return (
<CountProvider>
<CountDisplay />
<LangDisplay />
<Counter />
<ChangeLang />
</CountProvider>
)
}
const rootElement = document.getElementById('root')
render(<App />, rootElement)
count-context.tsx
import * as React from 'react'
type Action = {type: 'increment'} | {type: 'decrement'} | {type: 'switch'}
type Dispatch = (action: Action) => void
type State = {count: number; lang: string}
type CountProviderProps = {children: React.ReactNode}
const CountStateContext = React.createContext<State | undefined>(undefined)
const CountDispatchContext = React.createContext<Dispatch | undefined>(
undefined,
)
function countReducer(state: State, action: Action) {
switch (action.type) {
case 'increment': {
return {...state, count: state.count + 1}
}
case 'switch': {
return {...state, lang: state.lang === 'en' ? 'ro' : 'en'}
}
default: {
throw new Error(`Unhandled action type: ${action.type}`)
}
}
}
function CountProvider({children}: CountProviderProps) {
const [state, dispatch] = React.useReducer(countReducer, {
count: 0,
lang: 'en',
})
const stateRef = React.useRef(state)
React.useEffect(() => {
const customEvent = new CustomEvent('update', {
detail: {count: state.count},
})
document.dispatchEvent(customEvent)
}, [state.count])
React.useEffect(() => {
const customEvent = new CustomEvent('update', {
detail: {lang: state.lang},
})
document.dispatchEvent(customEvent)
}, [state.lang])
return (
<CountStateContext.Provider value={stateRef.current}>
<CountDispatchContext.Provider value={dispatch}>
{children}
</CountDispatchContext.Provider>
</CountStateContext.Provider>
)
}
function useState() {
const context = React.useContext(CountStateContext)
if (context === undefined) {
throw new Error('useCount must be used within a CountProvider')
}
return context
}
function useDispatch() {
const context = React.useContext(CountDispatchContext)
if (context === undefined) {
throw new Error('useDispatch must be used within a AccountProvider')
}
return context
}
export {CountProvider, useState, useDispatch}
答案 2 :(得分:0)
有一些方法可以避免重新渲染,也可以使您的状态管理“像redux一样”。我将向您展示我的工作方式,它远不是redux,因为redux提供了许多功能,这些功能并不是那么容易实现的,例如能够从任何动作或CombineReducers向任何reducer分配动作等功能。还有很多。
export const initialState = {
...
};
export const reducer = (state, action) => {
...
};
export const AppContext = React.createContext({someDefaultValue})
export function ContextProvider(props) {
const [state, dispatch] = useReducer(reducer, initialState)
const context = {
someValue: state.someValue,
someOtherValue: state.someOtherValue,
setSomeValue: input => dispatch('something'),
}
return (
<AppContext.Provider value={context}>
{props.children}
</AppContext.Provider>
);
}
function App(props) {
...
return(
<AppContext>
...
</AppContext>
)
}
这样,仅当这些特定的依赖项更新为新值时,它们才会重新渲染
const MyComponent = React.memo(({
somePropFromContext,
setSomePropFromContext,
otherPropFromContext,
someRegularPropNotFromContext,
}) => {
... // regular component logic
return(
... // regular component return
)
});
function select(){
const { someValue, otherValue, setSomeValue } = useContext(AppContext);
return {
somePropFromContext: someValue,
setSomePropFromContext: setSomeValue,
otherPropFromContext: otherValue,
}
}
function connectToContext(WrappedComponent, select){
return function(props){
const selectors = select();
return <WrappedComponent {...selectors} {...props}/>
}
}
import connectToContext from ...
import AppContext from ...
const MyComponent = React.memo(...
...
)
function select(){
...
}
export default connectToContext(MyComponent, select)
<MyComponent someRegularPropNotFromContext={something} />
//inside MyComponent:
...
<button onClick={input => setSomeValueFromContext(input)}>...
...
MyComponent
仅在上下文道具以新值更新时重新渲染,否则它将停留在该位置。
select
中的代码将在每次上下文更新中的任何值运行时运行,但是它什么都不做而且很便宜。
我建议您查看Preventing rerenders with React.memo and useContext hook.