Javascript 反应上下文API并避免重新呈现
我已经用底部的更新更新了这个 有没有一种方法可以维护一个单一的根状态(比如Redux),让多个上下文API使用者处理其提供者值的自己部分,而不触发对每个独立更改的重新呈现 我已经尝试了一些变体来测试那里提供的一些见解,但我仍然对如何避免重新渲染感到困惑 完整代码如下所示,并在此处联机: 问题是,根据devtools,每个组件都会看到一个“更新”(重新渲染),即使Javascript 反应上下文API并避免重新呈现,javascript,reactjs,react-redux,Javascript,Reactjs,React Redux,我已经用底部的更新更新了这个 有没有一种方法可以维护一个单一的根状态(比如Redux),让多个上下文API使用者处理其提供者值的自己部分,而不触发对每个独立更改的重新呈现 我已经尝试了一些变体来测试那里提供的一些见解,但我仍然对如何避免重新渲染感到困惑 完整代码如下所示,并在此处联机: 问题是,根据devtools,每个组件都会看到一个“更新”(重新渲染),即使SectionB是唯一看到任何渲染更改的组件,即使b是状态树中唯一更改的部分。我用功能组件和PureComponent尝试过这一点,看到
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>);
}
}
import React,{Component,createContext}来自'React';
const defaultState={
a:{x:1,y:2,z:3},
b:{x:4,y:5,z:6},
递增Bx:()=>{}
};
let Context=createContext(defaultState);
类应用程序扩展组件{
构造函数(…参数){
超级(…args);
此.state={
…默认状态,
incrementBX:this.incrementBX.bind(this)
}
}
递增bx(){
设{b}=this.state;
设newB={…b,x:b.x+1};
this.setState({b:newB});
}
render(){
返回(
);
}
}
导出默认应用程序;
类SectionA扩展组件{
render(){
返回({
({a})=>{a.x}
});
}
}
类SectionB扩展了组件{
render(){
返回({
({b})=>{b.x}
});
}
}
类SectionC扩展组件{
render(){
返回({
({incrementBX})=>增量a x
});
}
}
编辑:我知道可能会检测或显示重新渲染。我已经以某种方式展示了问题。我现在无法判断我所做的是否真的导致了重新渲染。根据我从丹·阿布拉莫夫(Dan Abramov)那里读到的内容,我认为我正确地使用了提供者和消费者,但我无法确定这是否属实。我欢迎任何见解。据我所知,上下文API并不是为了避免重新渲染,而是更像是Redux。如果希望避免重新渲染,可以查看
PureComponent
或lifecycle hookshouldComponentUpdate
这是一个提高性能的好方法,您也可以将其应用到上下文API中有一些方法可以避免重新渲染,还可以使您的状态管理“像redux一样”。我将向您展示我是如何做的,它远不是一个redux,因为redux提供了很多功能,这些功能的实现并不是那么简单,比如从任何操作或组合还原器向任何还原器发送操作的能力,以及许多其他功能 创建减速器 创建ContextProvider组件 具有从上下文中选择道具的功能(如redux map…) 编写一个connectToContext 用法
//MyComponent内部:
...
setSomeValueFromContext(输入)}>。。。
...
我在其他StackOverflow问题上做的演示
避免了重新渲染
MyComponent
仅当上下文中的细节道具更新为新值时才会重新渲染,否则它将保留在那里。
每当上下文中的任何值更新时,select
中的代码都会运行,但它什么也不做,而且价格便宜
其他解决方案
我建议你看看这个我做了一个关于如何从
React.Context
中获益的概念证明,但是避免重新呈现使用Context对象的子对象。该解决方案使用了React.useRef
和CustomEvent
。无论何时更改count
或lang
,只有使用特定属性的组件才会得到更新
请在下面查看,或尝试
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)
}
}, [])
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)
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}
import*作为来自“React”的React
从'react dom'导入{render}
从“/count context”导入{CountProvider,useDispatch,useState}
函数useConsumer(属性:“lang”|“count”){
const contextState=useState()
const[state,setState]=React.useState(contextState[prop])
const listener=(e:CustomEvent)=>{
if(e.detail和e.detail中的prop){
设置状态(如详图[prop])
}
}
React.useffect(()=>{
document.addEventListener('update',listener)
return()=>{
document.removeEventListener('update',listener)
}
}, [])
返回状态
}
函数CountDisplay(){
常量计数=使用消耗('count')
console.log('CountDisplay()',count)
返回(
{`当前计数为${count}`}
)
}
函数显示(){
常量lang=useConsumer('lang')
console.log('LangDisplay()',lang)
返回{`语言计数为${lang}`}
}
函数计数器(){
const dispatch=usedpatch()
返回(
分派({type:'increment'})}>
增量计数
)
}
函数ChangeLang(){
const dispatch=usedpatch()
返回分派({type:'switch'})}>switch
}
函数App(){
返回(
)
}
const rootElement=document.getElementById('root'))
渲染(,根元素)
countcontext.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)
}
}, [])
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)
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}
import*作为来自“React”的React
类型操作={type:'increment'}{type:'decrement'}}{type:'switch'}
类型分派=(操作:操作)=>void
类型状态={count:number;lang:string}
类型CountProviderProps={children:React.ReactNode}
const CountStateContext=React.createC
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)}>...
...
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)
}
}, [])
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)
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}