2

Here is the diagram. ChildComponentB has a state - stateX. In ChildComponentA, once the event occurs, it will change the stateX in ChildComponentB.

If the ChildComponentA is the child component of ChildComponentB, then it's easy, just pass the setStateX as a prop to ChildComponentA. But in this case, it's not.

a brief description diagram

The real scenario is the following. I have a canvas component, there are some static Rectangles already there, once there are mouse move over the line of the Rectangles, I'd like to add the indicator lines to another child component of the canvas component.

Hence, the rectComponent is not the descendent of the distanceIndicatorsComponent. So I can't pass the setLines to RectComponent.

What's your approach to do that?

enter image description here

If I use useContext approach, will it work?

Thank you, @KonradLinkowski to provide your solution. Here is his code. However, useContext is still lifing the state up to ParentComponent.

import React, { useContext, createContext, useState } from "react";

const Context = createContext();

function ChildComponentA(props) {
  const { setStateX } = useContext(Context);

  return (
    <div>
      componentA button:{" "}
      <button onClick={() => setStateX((i) => i + 1)}>add</button>
    </div>
  );
}

function ChildComponentB(props) {
  const { stateX } = useContext(Context);

  return <div> stateX is {stateX} </div>;
}

export default function ParentComponent(props) {
  const [stateX, setStateX] = useState(0);
  return (
    <>
      <Context.Provider value={{ stateX, setStateX }}>
        <ChildComponentA> </ChildComponentA>
        <ChildComponentB> </ChildComponentB>
      </Context.Provider>
    </>
  );
}

Regarding the reusbility of the ComponentB i.e. distanceIndicatorsComponent in this scenario, it includes the JSX and the states plus the interface in which there are logic to change the states. The are all parts which should be reusable in the furture.

From OOP perspective, the lines (state) belongs to DistanceIndicatorsComponent, and the how to change the lines (Add Line in this case) should be also reusable logic which belongs to distanceIndicatorsComponent.

However, from React perspective, to lift the setLines (this is the interface triggered under some event) is not "good enough" from OOP perspective. To lift the state - lines and state management function - setLines up to CanvasComponent is a "not good enough" in terms of the encapsulation. Put a wrap component on top of ComponentB is the same thing, the setLines still can't be passed to FrameComponent unless FrameComponent is a child-component of the wrap component.

It's very common to see there is a very heavy component holding all the state and the events at the top. It makes me feel that's a bad smell of the code. The reusability of the component should be based on a set of components, in this set of components, there is one uncontrolled component at the top, and underneath of this uncontrolled component are controlled components. This set of components is a external reusability unit.

Here, in this diagram, there should be more than one reusable unit rather than one. If lift the state up to CanvasComponent, it makes all the components underneath are un-reusable. In some extents, you still can re-use the JSX of this component, but I'd say, in terms of reusablity, it should invovle as many reusable logic as possible.

I might be wrong, please correct me. And thank you for sharing your valuable comments.

cdhit
  • 1,384
  • 1
  • 15
  • 38
  • Use `useContext` – Konrad Oct 10 '22 at 20:26
  • 1
    Keep the state in the parent component instead. Basic but required reading here: https://reactjs.org/docs/lifting-state-up.html – Robin Zigmond Oct 10 '22 at 20:30
  • 3
    Your component will usually have at least one distans parent in common. So move the state up to there. If they don't, then you need some kind of global state. The number of state libraries to handle that are abundant. – super Oct 10 '22 at 20:33
  • @RobinZigmond It's not good design in terms of the capsulation. The state - Lines don't belong to the canvasComponent. Hence, I don't want to lift the state up. – cdhit Oct 10 '22 at 20:33
  • @KonradLinkowski Are you sure? Could you talk about it in details? – cdhit Oct 10 '22 at 20:35
  • 1
    @cdhit regardless of whether you "want to" or not, it's the only way to do it. (The other approaches mentioned in other comments all end up doing the same, even though they may "look" different.) If you need state in 2 components to be related, you have to store it in a common parent. React's one-way data flow does not allow any other way. – Robin Zigmond Oct 10 '22 at 20:36
  • 1
    With context, you will store data in the common ancestor, but you won't pass props down directly which is convenient and reduce number of renders. – Konrad Oct 10 '22 at 20:38
  • @KonradLinkowski Can I use distanceIndicatorsComponet as the context of frameComponent. So RectComponent can use distanceIndicatorsComponet.setLines as a context? How to implement that? – cdhit Oct 10 '22 at 21:37
  • Move `setLines` to the context. use`lines` in `distanceIndicatorsComponet `, use`setLines` in `RectComponent ` – Konrad Oct 10 '22 at 21:40
  • I found it links to this https://stackoverflow.com/questions/62366824/how-can-%c4%b1-pass-function-with-state-for-react-usecontext# will close this if it applies – cdhit Oct 11 '22 at 20:16
  • Here is a test version. https://codesandbox.io/s/react-playground-forked-g31xb1?file=/parent_component.js:365-378 – cdhit Oct 11 '22 at 21:18
  • @KonradLinkowski I I still can't figure out your solution. How to pass the setLines in distanceIndicatorsComponet up one level to CanvasComponent, then use it there as a context and then use setLines in RectComponent. – cdhit Oct 11 '22 at 21:21
  • Updated your sandbox to make it work https://codesandbox.io/s/react-playground-forked-775bwl?file=/parent_component.js – Konrad Oct 11 '22 at 21:25
  • @KonradLinkowski oh, that's good. But, however, you still lift the state from ChildComponentB to ParentComponent. From design perspective, the ChildComponentB is the state holder rather than ParentComponent. The ParentComponent only functions as a bridge to link the ChildComponentB with ChildComponentA and provide the context to ChildComponentA. – cdhit Oct 11 '22 at 21:39
  • The state is defined in the parent component – Konrad Oct 11 '22 at 21:44
  • @KonradLinkowski That's not a good encapsulation, you know? It's hard to re-use the componentB and aslo doesn't make sense. But thank you anyway. – cdhit Oct 11 '22 at 21:46
  • @cdhit "*From design perspective, the ChildComponentB is the state holder rather than ParentComponent*" - well no, not any longer. What would ChildComponentA do if no ChildComponentB was rendered? What should happen if multiple ChildComponentB were rendered? With the state in the parent, you decide which and how many components in the tree share their state. – Bergi Oct 12 '22 at 22:12
  • @cdhit "*The ParentComponent should only function as a bridge*" - you could also achieve that, by having a `ref` object passed from the parent context down to all child components, and make them communicate through this in any manner that you like - e.g. passing the `setState` function from ChildComponentB to ChildComponentA. But you'll run into similar problems when multiple state-holding components compete for the same communication channel. At some point, just bite the bullet and use a proven react state management library instead of inventing your own. – Bergi Oct 12 '22 at 22:17
  • @Bergi Hi thank you for your input. It's a good point. However, it's a different topic of reusability of ComponentA rather than ComponentB. To put the state in ParentComponent, this makes both of CompA and CompB not reusable. It makes them as a whole, either have them or not at all. In terms of Reusable of CompA, maybe use high-order-component to convert a CompA to accept the Props - provided from CompB. But it's a different topic. – cdhit Oct 12 '22 at 22:39
  • @Bergi I haven't understand this approach when you say use - ref. And multiple state-holding components compete for the same communication channel. Could you talk about this a bit more? And what library do you think fit in this scenario? Thanks. – cdhit Oct 12 '22 at 22:44
  • It looks like Recoil fits into this scenario. https://recoiljs.org/docs/basic-tutorial/atoms – cdhit Oct 13 '22 at 22:14

4 Answers4

1

If keeping the shared state in the ParentComponent is the problem, you can extract the Context.Provider to a separate component and pass components as it's children, those children can access the context value via useContext hook.

function ParentContextProvider({ children }) {
  const [stateX, setStateX] = useState(0);

  return (
    <Context.Provider value={{ stateX, setStateX }}>
      {children}
    </Context.Provider>
  );
}

export default function ParentComponent(props) {
  return (
    <ParentContextProvider>
      <ChildComponentA />
      <ChildComponentB />
    </ParentContextProvider>
  );
}

Now you can add any new state/setState to the ParentContextProvider and can pass it to it's children

Shan
  • 1,468
  • 2
  • 12
1

Have you looked at Redux stores? You could have a variable like "showLine" or "originX"/"originY", then have one child dispatch changes, and the other child useSelector for the values?

Do you know if Redux works for your use case?

Julianna
  • 152
  • 6
  • Because Redux uses a single global state object, I am not feeling it's right to use it. I don't feel it's a thing to concern in the global level. And also Redux is too heavey to do such a little thing. – cdhit Oct 15 '22 at 21:11
1

I prefer to use a simple events pattern for this type of scenario. Eg using a component such as js-event-bus.

CHILD COMPONENT A

props.eventBus.emit('MouseOverRectangle', null, new MyEvent(23));

CHILD COMPONENT B

useEffect(() => {
    startup();
    return () => cleanup();
}, []);

function startup() {
    props.eventBus.on('MouseOverRectangle', handleEvent);
}

function cleanup() {
    props.eventBus.detach('MouseOverRectangle', handleEvent);
}

function handleEvent(e: MyEvent) {
    // Update state of component B here
}

RESULTS

This tends to result in quite clean encapsulation and also simple code. Eg any React conponent can communicate with any other, without needing to reveal internal details.

Gary Archer
  • 22,534
  • 2
  • 12
  • 24
  • This solution looks very nice. Looking forward to this. Hope it integrates with React very well. Thank you – cdhit Oct 18 '22 at 22:49
  • Do you think if there is any React component/plug-in using the same/similar js-event-bus approach underneath? If so, that should provide more robust implementation. Thanks @GaryArcher – cdhit Oct 18 '22 at 23:01
  • I'm not aware of any, though feel free to have a search. It is a design pattern really, so any equivalent library will work. Eg I use [this similar library](https://greenrobot.org/eventbus/documentation/how-to-get-started/) in Android apps. – Gary Archer Oct 19 '22 at 12:03
  • Thank you @GaryARcher. I created a sandbox here. https://codesandbox.io/s/react-playground-forked-0qbh0q?file=/parent_component.js It is working. Once I clicked the button in ComponentA, the event is triggered to change the original value in ComponentB from "componentB" to "from ComponentA". – cdhit Oct 22 '22 at 08:12
1

Requirements

First let us sum up the requirements.

Rect Component and Distance Indicators have not much to do with each other. Making them aware of each other or creating a dependency between them would be not desired in a good OOP design.

The interaction between both is very specific. Establishing a mechanism or a data structure just for this special sort of interaction would add an overhead to all components that don't need this sort of interaction.

General Concepts

So you must use a mechanism that is so generic that it does not add any sort of coupling. You need to establish something between these two components, which only these two components know and which for all the rest of your program is nonsense. What mechanisms serve for such a purpose?

  • Function pointers
  • Lambda functions
  • Events

Function pointers and lambda functions are complicated constructs. Not everybody prefers to use them. Now you see why events are so popular. They address a common requirement of connecting two components without revealing any of the details of them to anybody.

I personally recommend you to use lambda functions in this situation. Because this is one strength of JavaScript. Search in google for callback or asynchronous lambda function. This often adds the least overhead to existing code. Because a lambda functions has an important property:

With lambda functions you can do things very locally. Doing things locally is an important design principle. You don't need to define extra methods or functions or classes. You can just create them wherever you are, return them, pass them freely around to where you actually need them and store them there. You can store them even without knowing what is behind them.

I think, this is your answer. The only thing you need is a mechanism to pass lambda functions and to store your lambda functions. But this is on a very generic level and therefore adds no coupling.

With events you are on similar path. The event mechanism is already there. But therefore you already have a good answer.

Example with pure JavaScript

When applying this to JavaScript we can imagine that function pointers could be compared to function expressions in JavaScript. And lambda functions can be compared to arrow functions in JavaScript. (Note: Arrow functions also provide "closures", which is required in this case, see How do JavaScript closures work?).

A simple example illustrates this:

class DistanceIndicator {
  constructor(height, width) {
    this.height = height;
    this.width = width;
  }
  
  resize(height){
      this.height = height;
  }

  incorrect_resizer(height){
      return this.resize;
  }
  
  resizer(){
      return (height) => this.resize(height); 
  }
  
  resizer_with_less_overhead(){
      return (height) => this.height = height; 
  }
}

p = new DistanceIndicator();

p.resize(19);
// If you want to use this, you have to store p. You may see
// this as not so nice, because, you are not interested in what 
// actually p is. And you don't want to expose the information
// that it has a method resize. You want to have the freedom
// of changing such details without the need of changing all
// the code where something happens with Rectangles.
console.log(p.height);

resizer = p.incorrect_resizer()
//resizer(18);
// In this example, resizer is a function pointer. It would be
// nice to store it and be able to call it whenever we want to
// inform Rectangle about something interesting. But it does not
// work because the resize method cannot be isolated from the
// class. The "this" is not there.
console.log(p.height);

resizer = p.resizer();
resizer(17);
// That works. Lambda functions do the job. They are able to
// include the "this" object.
console.log(p.height);

resizer = p.resizer_with_less_overhead();
resizer(16);
console.log(p.height);

// As you have now a resizer, you can store it wherever you want.
// You can call it without knowing what is behind it.

The idea in the example is that you can store the resizers wherever you want without knowing what they are. You shouldn't name them resizer, but give them a generic name like size_notification.

Example for React

The React concept for contexts is a typical candidate for data exchange between components. But the principle of React is a pure unidirectional data flow (top-down). This is also true for the context, which means, we cannot use a context for what we want.

React does not provide support for the implementation of the proposed idea. React is only responsible for the pure construction of the HTML page and a comfortable and performant rendering. It is not responsible for the "business" logic of our HTML page. This is done in full JavaScript. That makes sense because you want be able to develop complex web applications. Therefore you need all your favourite programming concepts. A real application does not follow the design principle of React. React is only a presentation layer. Most people like OOP progamming.

So when implementing something with React we must keep in mind that React is just a library for JavaScript. The full power of JavaScript is always available and should be used for our web application.

After realizing this, the problem becomes simple. See this code:

import React from 'react';

let sizeNotificator = (newValue) => {console.log(newValue)};

function Rect(props) {
  return <button onClick={() => sizeNotificator("12")}>resize to 12</button>;
}

class DistanceIndicator extends React.Component {

  state = {
    size: "0",
  };

  setSize(newValue) {
    this.setState({
        size : newValue
    });
  };

  componentDidMount(){
    sizeNotificator = ((newValue) => {this.setSize(newValue);})
  }

  render() {
    return <p>Current size: { this.state.size}</p>;
  }

}

class App extends React.Component {

  render() {
    return(<div>
             <DistanceIndicator/>
             <Rect/>
           </div>);
  }
  
}

export default App;

With this code the requirement is fulfilled that none of the DistanceIndicator implementation details are revealed to the outside of DistanceIndicator.

Obviously this example code only works if there is not more than one DistanceIndicator. To solve this is a different topic with probably not only one good solution.

habrewning
  • 735
  • 3
  • 12
  • Can you pls demo some code around the lambda function solution? @habrewning – cdhit Oct 20 '22 at 01:44
  • There you are. That is a nice and important question. You will see in the example that with the lambda function objects are very comfortable. They just work regardless where and how you store them. Your only decision now is where you create, pass and store them. And that decision nobody can make for you. That is a decision decision. All the rest is just technical details. – habrewning Oct 20 '22 at 07:35
  • Thanks in advance. It will take some time for me to consume this knowledge. Rely later. Cheers @habrewning – cdhit Oct 20 '22 at 20:26
  • Thanks @habrewning, I have a couple of questions. Because the scenario is React, so here the Lambda function located in the React class component, then initialize the React class component to get an instance, pass this instance to the RectComponent so in RectComponent trigger the event. In DistanceIndicator's upper component has to use it's instance rather than use a JSX approach. It seems fine in a normal javascript program but it seems weird in a React app. And, class component is not a preferable way to in React, do you think if using React function component can achieve same? – cdhit Oct 22 '22 at 06:12
  • I mean put the lamba function inside the React function component - DistanceIndicator and pass the function component to RectComponent so it can be triggered from there? – cdhit Oct 22 '22 at 06:13
  • It is easier that you think. Just keep in mind, that React is only a library. We are doing JavaScript. I am updating my answer and add an example. – habrewning Oct 23 '22 at 12:25