如何在 React Context 中实现 observable 观察值

Posted

技术标签:

【中文标题】如何在 React Context 中实现 observable 观察值【英文标题】:How to implement observable watching for value in React Context 【发布时间】:2021-03-01 07:57:10 【问题描述】:

假设我有一个Parent 组件,它提供了一个Context,它是一个Store 对象。为简单起见,假设这个 Store 有一个 value 和一个 update this value

的函数
class Store 
// value

// function updateValue() 



const Parent = () => 
  const [rerender, setRerender] = useState(false);
  const ctx = new Store();

  return (
    <SomeContext.Provider value=ctx>
      <Children1 />
      <Children2 />
      .... // and alot of component here
    </SomeContext.Provider>
  );
;

const Children1 = () => 
 const ctx = useContext(SomeContext);
 return (<div>ctx.value</div>)


const Children2 = () => 
 const ctx = useContext(SomeContext);
 const onClickBtn = () => ctx.updateValue('update')
 return (<button onClick=onClickBtn>Update Value </button>)

所以基本上Children1会显示值,而在Children2组件中,有一个更新值的按钮。

所以我现在的问题是当Children2 更新 Store 值时,Children1 不会重新渲染。 以反映新值。

堆栈溢出的一种解决方案是here。这个想法是在Parent 中创建一个state 并使用它将context 传递给孩子们。这将有助于重新渲染 Children1 因为 Parent 被重新渲染。 但是,我不想 Parent 重新渲染,因为在Parent 中有很多其他组件。我只想重新渲染Children1

那么有什么办法可以解决这个问题吗?我应该使用 RxJS 进行响应式编程还是应该更改代码中的某些内容?谢谢

【问题讨论】:

也许你可以在这里获得更多信息。 ***.com/questions/50817672/… updateValue('update') 它看起来怎么样?似乎可能存在无法更新正确对象属性的问题。 【参考方案1】:

你可以使用像 redux lib 这样的上下文,如下所示

这很容易使用,以后如果您想迁移到 redux,您只需更改 store 文件,整个状态管理将被迁移到 redux 或任何其他库。

运行示例: https://stackblitz.com/edit/reactjs-usecontext-usereducer-state-management

文章:https://rsharma0011.medium.com/state-management-with-react-hooks-and-context-api-2968a5cf5c83

Reducers.js

import  combineReducers  from "./Store";

const countReducer = (state =  count: 0 , action) => 
  switch (action.type) 
    case "INCREMENT":
      return  ...state, count: state.count + 1 ;
    case "DECREMENT":
      return  ...state, count: state.count - 1 ;
    default:
      return state;
  
;

export default combineReducers( countReducer );

Store.js

import React,  useReducer, createContext, useContext  from "react";

const initialState = ;
const Context = createContext(initialState);

const Provider = ( children, reducers, ...rest ) => 
  const defaultState = reducers(undefined, initialState);
  if (defaultState === undefined) 
    throw new Error("reducer's should not return undefined");
  
  const [state, dispatch] = useReducer(reducers, defaultState);
  return (
    <Context.Provider value= state, dispatch >children</Context.Provider>
  );
;

const combineReducers = reducers => 
  const entries = Object.entries(reducers);
  return (state = , action) => 
    return entries.reduce((_state, [key, reducer]) => 
      _state[key] = reducer(state[key], action);
      return _state;
    , );
  ;
;

const Connect = (mapStateToProps, mapDispatchToProps) => 
  return WrappedComponent => 
    return props => 
      const  state, dispatch  = useContext(Context);
      let localState =  ...state ;
      if (mapStateToProps) 
        localState = mapStateToProps(state);
      
      if (mapDispatchToProps) 
        localState =  ...localState, ...mapDispatchToProps(dispatch, state) ;
      
      return (
        <WrappedComponent
          ...props
          ...localState
          state=state
          dispatch=dispatch
        />
      );
    ;
  ;
;

export  Context, Provider, Connect, combineReducers ;

App.js

import React from "react";
import ContextStateManagement from "./ContextStateManagement";
import CounterUseReducer from "./CounterUseReducer";
import reducers from "./Reducers";
import  Provider  from "./Store";

import "./style.css";

export default function App() 
  return (
    <Provider reducers=reducers>
      <ContextStateManagement />
    </Provider>
  );

组件.js

import React from "react";
import  Connect  from "./Store";

const ContextStateManagement = props => 
  return (
    <>
      <h3>Global Context: props.count </h3>
      <button onClick=props.increment>Global Increment</button>
      <br />
      <br />
      <button onClick=props.decrement>Global Decrement</button>
    </>
  );
;

const mapStateToProps = ( countReducer ) => 
  return 
    count: countReducer.count
  ;
;

const mapDispatchToProps = dispatch => 
  return 
    increment: () => dispatch( type: "INCREMENT" ),
    decrement: () => dispatch( type: "DECREMENT" )
  ;
;

export default Connect(mapStateToProps, mapDispatchToProps)(
  ContextStateManagement
);

【讨论】:

【参考方案2】:

如果您不希望您的 Parent 组件在状态更新时重新渲染,那么您使用了错误的状态管理模式,完全没有问题。相反,您应该使用 Redux 之类的东西,它从 React 组件树中完全删除“状态”,并允许组件直接订阅状态更新。

Redux 将允许 only 订阅特定存储值的组件在这些值更新时更新 only。因此,您的父组件和调度更新操作的子组件不会更新,而只有订阅状态更新的子组件。效率很高!

https://codesandbox.io/s/simple-redux-example-y3t32

【讨论】:

您好,感谢您的推荐。实际上我的意思不是我不希望Parent 在其状态更改时重新渲染,而是在context 更改时重新渲染。而Parent 只是负责将上下文注入孩子。除非您的观点是 context 始终需要映射到 Parent 状态,否则它是另一回事 就我而言,我不能使用 Redux,因为我不是在构建应用程序,而是在构建组件库,因此在内部使用 Redux 并不常见。 @JakeLam 这正是我的观点,使用 Redux,您可以在不重新渲染 Parent 的情况下更新您的应用程序状态,因为它没有订阅这些值。你看我的例子了吗?此外,据我所知,没有什么能阻止您在组件库中使用 Redux。您只需要确保所有组件都使用相同的 Redux 存储 - 与 Context 没有太大不同,您必须确保所有组件都可以访问相同的上下文。【参考方案3】:

React 组件仅在以下任一情况下更新

    自己的props被改了 state改了 父母的state已更改

正如您所指出的,state 需要保存在父组件中并传递给上下文。

你的要求是

    state 更改时,父级不应重新渲染。 只有 Child1 应该在 state 更改时重新渲染
const SomeContext = React.createContext(null);

孩子 1 和 2

const Child1 = () => 
  const ctx = useContext(SomeContext);

  console.log(`child1: $ctx`);

  return <div>ctx.value</div>;
;
const Child2 = () => 
  const ctx = useContext(UpdateContext);

  console.log("child 2");

  const onClickBtn = () => 
    ctx.updateValue("updates");
   
  ;

  return <button onClick=onClickBtn>Update Value </button>;
;

现在是添加 state 的上下文提供程序

const Provider = (props) => 
  const [state, setState] = useState( value: "Hello" );

  const updateValue = (newValue) => 
    setState(
      value: newValue
    );
  ;

  useEffect(() => 
    document.addEventListener("stateUpdates", (e) => 
      updateValue(e.detail);
    );
  , []);

  const getState = () => 
    return 
      value: state.value,
      updateValue
    ;
  ;

  return (
    <SomeContext.Provider value=getState()>
      props.children. 
    </SomeContext.Provider>
  );
;

同时呈现Child1Child2 的父组件

const Parent = () => 
 // This is only logged once
  console.log("render parent");

  return (
      <Provider>
        <Child1 />
        <Child2 />
      </Provider>
  );
;

现在,当您通过单击child2 中的按钮更新state 时,第一个要求Parent 不会重新渲染,因为Context Provider 不是它的父级。

state 更改时,只有Child1Child2 会重新渲染。

现在对于第二个要求,只有 Child1 需要重新渲染。

为此,我们需要进行一些重构。

这就是反应性的来源。只要Child2 是 Provider 的子级,只要 state 发生更改,它也会得到更新。

Child2 从提供程序中取出。

const Parent = () => 
  console.log("render parent");

  return (
    <>
      <Provider>
        <Child1 />
      </Provider>
      <Child2 />
    </>
  );
;

现在我们需要一些方法来更新 Child2 的状态。

为了简单起见,我在这里使用了浏览器自定义事件。你可以使用 RxJs。

Provider 正在监听状态更新,Child2 将在单击按钮并更新状态时触发事件。

const Provider = (props) => 
  const [state, setState] = useState( value: "Hello" );

  const updateValue = (e) => 
    setState(
      value: e.detail
    );
  ;

  useEffect(() => 
    document.addEventListener("stateUpdates", updateValue);


return ()=>
   document.addEventListener("stateUpdates", updateValue);

  , []);

  return (
    <SomeContext.Provider value=state>props.children</SomeContext.Provider>
  );
;
const Child2 = () => 

  console.log("child 2");

  const onClickBtn = () => 
    const event = new CustomEvent("stateUpdates",  detail: "Updates" );

    document.dispatchEvent(event);
  ;

  return <button onClick=onClickBtn>Update Value </button>;
;

注意:Child2 将无法访问上下文

如果你有什么不明白的地方,我希望这有助于告诉我。

【讨论】:

以上是关于如何在 React Context 中实现 observable 观察值的主要内容,如果未能解决你的问题,请参考以下文章

[react] 写例子说明React如何在JSX中实现for循环

如何在 react-native 中实现具有透明背景的视频

如何在 React 中实现 Cloudinary 上传小部件?

如何在 React Native 中实现 Twilio android 推送通知?

如何在 React 中实现分页

如何在 VPS 上的 React 中实现 DRM