Javascript 反应上下文API并避免重新呈现

Javascript 反应上下文API并避免重新呈现,javascript,reactjs,react-redux,Javascript,Reactjs,React Redux,我已经用底部的更新更新了这个 有没有一种方法可以维护一个单一的根状态(比如Redux),让多个上下文API使用者处理其提供者值的自己部分,而不触发对每个独立更改的重新呈现 我已经尝试了一些变体来测试那里提供的一些见解,但我仍然对如何避免重新渲染感到困惑 完整代码如下所示,并在此处联机: 问题是,根据devtools,每个组件都会看到一个“更新”(重新渲染),即使SectionB是唯一看到任何渲染更改的组件,即使b是状态树中唯一更改的部分。我用功能组件和PureComponent尝试过这一点,看到

我已经用底部的更新更新了这个

有没有一种方法可以维护一个单一的根状态(比如Redux),让多个上下文API使用者处理其提供者值的自己部分,而不触发对每个独立更改的重新呈现

我已经尝试了一些变体来测试那里提供的一些见解,但我仍然对如何避免重新渲染感到困惑

完整代码如下所示,并在此处联机:

问题是,根据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>);
    }
}
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 hook
shouldComponentUpdate


这是一个提高性能的好方法,您也可以将其应用到上下文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}