How to implement observable watching for value in React Context
Asked Answered
C

4

8

Let's say I'm having a Parent Component providing a Context which is a Store Object. For simplicity lets say this Store has a value and a function to 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>)
}

So basically Children1 will display the value, and in Children2 component, there is a button to update the value.

So my problem right now is when Children2 updates the Store value, Children1 is not rerendered. to reflect the new value.

One solution on stack overflow is here. The idea is to create a state in Parent and use it to pass the context to childrens. This will help to rerender Children1 because Parent is rerendered. However, I dont want Parent to rerender because in Parent there is a lot of other components. I only want Children1 to rerender.

So is there any solution on how to solve this ? Should I use RxJS to do reative programming or should I change something in the code? Thanks

Cautious answered 18/11, 2020 at 3:10 Comment(2)
Maybe you can get more info here. #50818172Disillusionize
updateValue('update') how does it look like? It seems like issue may be there which may not be updating right object property.Lordosis
D
5

You can use context like redux lib, like below

This easy to use and later if you want to move to redux you change only the store file and the entire state management thing will be moved to redux or any other lib.

Running example: https://stackblitz.com/edit/reactjs-usecontext-usereducer-state-management

Article: 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>
  );
}

Component.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
);
Dobbins answered 18/11, 2020 at 11:57 Comment(0)
B
2

If you don't want your Parent component to re-render when state updates, then you are using the wrong state management pattern, flat-out. Instead you should use something like Redux, which removes "state" from the React component tree entirely, and allows components to directly subscribe to state updates.

Redux will allow only the component that subscribes to specific store values to update only when those values update. So, your Parent component and the Child component that dispatches the update action won't update, while only the Child component that subscribes to the state updates. It's very efficient!

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

Barcellona answered 18/11, 2020 at 6:32 Comment(3)
hi, thanks for your recommendation. Actually my point is not that i dont want Parent to re-render when its state change, but when context changes. And Parent is just hold the job to inject the context into children. Unless if your point is context always need to be mapped to the Parent state then its a different thingCautious
And for my case, i cannot use Redux because I'm not building an app, I'm building a component library, therefore having Redux inside is not really common.Cautious
@JakeLam That's exactly my point, with Redux you can update your app state without Parent re-rendering because it is not subscribed to the values. Did you look at my example? Also, as far as I know there is nothing stopping you from using Redux inside of a component library. You would just need to ensure that all components are using the same Redux store - not so different from Context where you have to ensure all components have access to the same context.Barcellona
S
2

React component is updated only when either

  1. Its own props is changed
  2. state is changed
  3. parent's state is changed

As you have pointed out state needs to be saved in the parent component and passed on to the context.

Your requirement is

  1. Parent should not re-render when state is changed.
  2. Only Child1 should re-render on state change
const SomeContext = React.createContext(null);

Child 1 and 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>;
};

Now the context provider that adds the 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>
  );
};

Parent component that renders both the Child1 and Child2

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

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

Now for the first requirement when you update the state by clicking button from the child2 the Parent will not re-render because Context Provider is not its parent.

When the state is changed only Child1 and Child2 will re-render.

Now for second requirement only Child1 needs to be re-rendered.

For this we need to refactor a bit.

This is where reactivity comes. As long as Child2 is a child of Provider when ever the state changes it will also gets updated.

Take the Child2 out of provider.

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

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

Now we need some way to update the state from Child2.

Here I have used the browser custom event for simplicity. You can use RxJs.

Provider is listening the state updates and Child2 will trigger the event when button is clicked and state gets updated.

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>;
};

NOTE: Child2 will not have access to context

I hope this helps let me know if you didn't understand anything.

Salters answered 18/11, 2020 at 6:37 Comment(0)
D
0

I now use the @webkrafters/react-observable-context instead of using the React Context Api directly.

Darwen answered 19/11, 2023 at 23:42 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.