facebook / react

The library for web and native user interfaces.
https://react.dev
MIT License
229.3k stars 46.96k forks source link

Sending events from parents to children easily #6646

Closed mosesoak closed 8 years ago

mosesoak commented 8 years ago

Been using React (Native) for half a year now, really enjoying it! I'm no expert but have run up against what seems like a weakness in the framework that I'd like to bring up.

The problem

Sending one-off events down the chain (parent-to-child) in a way that works with the component lifecycle.

The issue arises from the fact that props are semi-persistent values, which differs in nature from one-time events. So for example if a deep-link URL was received you want to say 'respond to this once when you're ready', not 'store this URL'. The mechanism of caching a one-time event value breaks down if the same URL is then sent again, which is a valid event case.

Children have an easy and elegant way to communicate back to parents via callbacks, but there doesn't seem to be a way to do this same basic thing the other direction.

Example cases

From everything I've read, the two normal ways to do this are 1) call a method on a child directly using a ref, or 2) emit an event that children may listen for. But those ignore the component lifecycle, so the child isn't ready to receive a direct call or event yet.

These also feel clunky compared to the elegance of React's architecture. But React is a one-way top-down model, so the idea of passing one-time events down the component chain seems like it would fit nicely and be a real improvement.

Best workarounds we've found

Is there is some React Way to solve this common need? If so, no one on our team knows of one, and the few articles I've found on the web addressing component communication only suggest dispatching events or calling methods directly refs. Thanks for the open discussion!

jimfb commented 8 years ago

The "React Way" is to store your application state higher up (eg. in a Flux store). React only renders your current state, so you never need to send "events" down to children. The "events" change your application state (eg. the scroll position), and you pass the current scroll position down as a prop.

Of course, React does allow you to model your application state at lower level components, and you can then get a ref to a the component and call functions on that component, but this has always been an escape hatch rather than a recommended pattern.

A general rule of thumb is that the parent component should never be telling a child to "do something", and should never be "asking" a child anything. Obviously there are exceptions to every rule (that's what refs are for) but as a first order approximation it's true. If you find yourself breaking this rule often, it's an indication that you should probably rethink the data flow of your application (and specifically, rethink where state is being stored).

mosesoak commented 8 years ago

@jimfb Thanks for the reply, I appreciate your time looking at my post.

I sort of posed my initial post as a question from a beginner, but I'd like to expand on it with a bit of architecture discussion if I may have a moment more of your time. (And then maybe you'd consider reopening this issue if you think I bring up a valid point.) I'm actually more of a veteran framework user, (like a decade or so). The dev team I'm part of does use Flux, and we understand the tenets of managing state well to keep child components simple and reusable. We're even keeping up with your team's insane release clip, which ain't easy! 😉 React is a nice piece of thought & engineering.

The rule of thumb you mentioned that parent components should not need to tell children directly to do something is solid, and covers maybe 95% of real-world dev cases. For the outlier cases that don't fit that pattern, as you mention, we can use refs. But the reality is that this doesn't work, and is a pretty non-React-like workaround for what I see as a normal, valid subset of use cases.

I noted a few examples in my original post that are worth thinking about a little. When receiving a deep link, child containers need to receive a piece of time-based information that is not necessarily unique (duplicates are allowed, it is the fact that the event is occurring that is unique, not the payload). Components need to receive this information after they've been navigated to and their lifecycle is ready to process it. Another example is list items in a parent scroll container that need to all animate at specific times, which also needs to play well with their lifecycles as items are added/removed/changed. "I need to animate now" is not a piece of state, it's a time-based message that doesn't need to be retained.

React's diffing engine (a great thing in itself) often makes these cases even more painful to solve. For time-based information, retention of state is often undesirable, a component just needs to know that it happened in a reliable way. Falling back on using refs or dispatching events does not work well when a child isn't ready, and those basically just let you go around the framework. Other hacks include setting-unsetting a state, incrementing a dummy integer state, or storing timestamps to manage state changes. These are all inelegant.

So here's my thought: these problem cases could be solved in a way that reinforces React's core values by offering some kind of top-down 'state-event' mechanism in addition to plain state. The one-way communication model could be expanded to include either stateful/persistent or one-time-use information. These are two distinctly different types of information that could be communicated in a similar way. Best practices could be defined around when to use which.

You guys have already proven out that the 95% case is covered by plain state which is awesome, but if in addition to this model you offered an official top-down event mechanism that was guaranteed to work with the component lifecycle, it would basically obviate the need for event dispatchers and refs.

What do you think, reopen this for discussion?

jimfb commented 8 years ago

... covers maybe 95% of real-world dev cases. For the outlier cases that don't fit that pattern, as you mention, we can use refs. But the reality is that this doesn't work, and is a pretty non-React-like workaround for what I see as a normal, valid subset of use cases.

For the other 5%, we're ok with you breaking out using one of the escape hatches. We're constantly working to reduce this 5% down to 1% and then down to 0.1%, but we're still getting there. The fact that it's non-react-like is a symptom of the fact that you're now working directly with the underlying platform, having escaped out of React. The most common use cases for escape hatches today are layout, animations, etc. We are actively working on these topics, and we have some ideas, but there are a lot of complexities that aren't necessary obvious at first glance and we want to make sure that we "get it right".

In general, however, React does a pretty good job covering the common use cases. Usually, when people complain that something is difficult or that they are breaking out of React too often, it's because their mental model is wrong. They're still thinking imperatively, instead of thinking declaratively. It's not always easy to retrain your brain, and so it's easy to fall into the imperative trap without even realizing it.

Another example is list items in a parent scroll container that need to all animate at specific times, which also needs to play well with their lifecycles as items are added/removed/changed. "I need to animate now" is not a piece of state, it's a time-based message that doesn't need to be retained.

That is the imperative way of looking at it. But there are other models that are equally valid, and perhaps even better (more flexible). For instance, suppose someone removes a node, but then a fraction of a second later decides "actually, we're aborting that operation, put it back!". You want the animation to effectively slow down and then reverse its self. The imperative "fire-and-forget" model for animations does not work well in that situation, but a declarative spring model (where the component's position is modeled by a spring whom's position is derived over time) works really well in the general case. Our current thinking is probably closest to what is described here: https://facebook.github.io/react-native/docs/animations.html

In the most general sense, the declarative way of looking at events is as a set (list) of timestamped objects. When a new event "occurs", it is simply added to the list. The list is a constant value, which may reasonably be passed as props. And now you've completely eliminated the need to "fire an event" in a sense.

The one-way communication model could be expanded to include either stateful/persistent or one-time-use information. These are two distinctly different types of information that could be communicated in a similar way. Best practices could be defined around when to use which.

The "two distinctly different types of information" aren't distinct, because you can convert between the two. "one-time-use information" becomes declarative "state" if you save it into a list.

I'm not saying that we would never go down such a route. Jordan had some wild ideas a year back about making all components a pure function of signals (events). (ie. nothing is stateful, everything is an event transform). This has some interesting properties, especially because it makes "history" and time-travel debugging super easy.

Anyway, it's a big design space, and we're thinking about it.

React's diffing engine (a great thing in itself) often makes these cases even more painful to solve.

Totally agree, there is no perfect solution. We try to make React as painless as we can.

I don't want to re-open, only because there is nothing actionable here at the moment. We understand the desire/use case, and we regularly have team discussions about such topics. But we use github issues for tracking actionable bugs (not discussions or questions). We use issues to track things that have a plan forward (or at least a strong candidate proposal). In this case, the only thing we know for sure is that we want to avoid anything that feels imperative (like an "event" "firing"). But rest assured, we are thinking about the problem.

Feel free to continue the discussion on this thread. I can't guarantee I'll respond to every post, but I do read pretty much everything that passes through github. Another place that such discussions can take place is https://discuss.reactjs.org/

mosesoak commented 8 years ago

@jimfb Cool, I will definitely join the discussion there and reserve GitHub for code issues in future, thanks again for taking the time to lay this out.

I understand your response completely and I think it's admirable that you guys are sticking to your guns to try and whittle down that last 5% in the most declarative way possible.

To be clear, I'm not asking for an imperative solution. Just highlighting a rough spot in using the framework and encouraging that some salve be applied in the most React way possible, so fully in keeping with a declarative approach. I'm suggesting that imperative solutions (like events) don't play well with React, but that there are still cases where time is the differentiator and duplicate values are acceptable.

I do like the 'timestamped list' model that you suggested, although it's a slightly funky way to have to work. I actually mentioned it in my response – "Other hacks include ... storing timestamps to manage state changes" – so I'm aware of it as a possible workaround. Since you guys are actively discussing this topic internally, I guess my contribution with this post would be to point out that from my perspective as a React dev, that feels like a workaround, it's clunky in practice.

If React made it easy to look up the timestamp of any prop without extra baggage, that might help with these types of situations. It might also strengthen React's diffing capabilities. It's hard to do though, without adding metadata to primitive values, not JavaScript's strong suit by any means. Anyway I'm not coming at this with a specific solution, just interested in raising it as an infrequent but notable problem area that a slight tweak to the capabilities of the framework might soothe.

Again, thanks very much for your thoughtful response! The React team's attitude of openness and inclusion is a breath of fresh air, and really sets a strong positive example for the community spirit of Open Source. Keep up the great work.

ostrovskyi-sergii commented 7 years ago

IMO there is need for something in react like 'event' prop type, which child component may listen. Just like regular prop types. @mosesoak thanks for good advise. I'm now using integer prop which is just incremented by parent and monitored by child in ComponentWillReceiveProps.

Andsbf commented 6 years ago

I have faced this same issue and used the increment props as a solution many times, but while reasoning about it one more time I thought about using callbacks, something like:

class MyComponent extends Component {
  constructor(props) {
    super(props);

    this.state = {
      formSubmittedSuccessfully: false,
      isLoading: false
    };
  }

  submitForm(form) {
    this.setState({isLoading: true})

    this.props.submitFormToApi(form, this.formSubmitionCallback)
  }

  formSubmitionCallback(response) {
    if (!response.error) {
      this.setState({formSubmittedSuccessfully: true})
    }
  }

  rednder() {
     ...
  }
}

it adds a bit coupling, as the component now has to know how to handle the response, but I believe that would be the price to pay for it.

what do you guys think?

Thanks in advance!

jecxjo commented 6 years ago

@Andsbf The callback method still requires more boilerplate and coupling between the parent and child that there should be.

The fact that you can use refs to accomplish the same output with almost exactly the syntax you want means that the props design is broken.

const { Component } = React;
const { render } = ReactDOM;

class Parent extends Component {
  render() {
    return (
      <div>
        <Child ref={instance => { this.child = instance; }} />
        <button onClick={() => { this.child.increment(); }}>Click</button>
      </div>
    );
  }
}

class Child extends Component {
    constructor(props) {
    super(props)
    this.state = { count: 0 }
  }

  increment() {
    let { count } = this.state
    count = count + 1
    this.setState({ count })
  }

  render() {
    return (
      <div>Count: {this.state.count}</div>
    );
  }
}

render(
  <Parent />,
  document.getElementById('container')
);

The child component should be able to contain its own state, something that the parent doesn't need to know anything about, and should be able to receive an event that tells it to do something with that state.

I'm working on a component that shows sequences of data. The component handles the transition from one sequence to another. The parent object doesn't care about what the data is, or how its cycled, however it would like to be able to trigger an event where the child resets back to the first item, or move to the last item. It smells so bad to have to increment a property to tell the child to perform an action where the property isn't even used. It smells even worse if using this sequence component requires the parent to actually handle the movement through the sequence.

What is the point of the component if I'm just putting all the logic in the parent? I should just feed all the data into the child and let it do its thing and if I need to poke at it, it should understand what to do.

ozydingo commented 5 years ago

I want to add another use case that I have to believe is reasonably common, to the point that I'd have a hard time believing you can really get down to 1% or 0.1% without address the issue as originally stated. However if there is a declarative way of thinking about this I'm all ears; I'm also a bit new to this style of thinking.

In my case, I have an element that contains a <video> tag, and a sibling element that mirrors the video timeline (in my case, displaying a history of metrics computed from the video). I want a click on the sibling timeline to cause the video to seek to the same position (but continue playing). I don't know how to do this other than to send an "event" to the child (or, in React, a prop with a unique timestamp or id, essentially amounting to a command queue of size 1). Storing what is really a one-off event as a prop along with an id to ensure one-time execution, as others have said above, smells so wrong, and is a lot of extra code to work around what could be so simple. Again, would love to learn of a different way to think about it.

But the "seek" event is inherently a transient thing. Immediately after you declare that the video time should be set to t, it is changed by the video itself. I suppose this is similar to the spring animation, but the position of the video time playhead is inherently controller by the child, not the parent. Surely the solution is not to actually try to yield control of the video's playhead at every frame, accounting for buffering, playback speed, etc, to the relatively dumb parent element, right? This is all handled extremely well by the <video> element itself, and I'd argue should be. So what's the declarative way of causing a child component to have its <video> element to seek to a specified position?

gaearon commented 5 years ago

This is a very old thread and is not being tracked by anyone. Most of advice in it is probably also outdated. If you want to discuss this again, please create a new issue. Thanks.

pranav-bharadwaj commented 3 years ago

@Andsbf The callback method still requires more boilerplate and coupling between the parent and child that there should be.

The fact that you can use refs to accomplish the same output with almost exactly the syntax you want means that the props design is broken.

const { Component } = React;
const { render } = ReactDOM;

class Parent extends Component {
  render() {
    return (
      <div>
        <Child ref={instance => { this.child = instance; }} />
        <button onClick={() => { this.child.increment(); }}>Click</button>
      </div>
    );
  }
}

class Child extends Component {
  constructor(props) {
      super(props)
    this.state = { count: 0 }
  }

  increment() {
    let { count } = this.state
    count = count + 1
    this.setState({ count })
  }

  render() {
    return (
      <div>Count: {this.state.count}</div>
    );
  }
}

render(
  <Parent />,
  document.getElementById('container')
);

The child component should be able to contain its own state, something that the parent doesn't need to know anything about, and should be able to receive an event that tells it to do something with that state.

I'm working on a component that shows sequences of data. The component handles the transition from one sequence to another. The parent object doesn't care about what the data is, or how its cycled, however it would like to be able to trigger an event where the child resets back to the first item, or move to the last item. It smells so bad to have to increment a property to tell the child to perform an action where the property isn't even used. It smells even worse if using this sequence component requires the parent to actually handle the movement through the sequence.

What is the point of the component if I'm just putting all the logic in the parent? I should just feed all the data into the child and let it do its thing and if I need to poke at it, it should understand what to do. it

@Andsbf The callback method still requires more boilerplate and coupling between the parent and child that there should be.

The fact that you can use refs to accomplish the same output with almost exactly the syntax you want means that the props design is broken.

const { Component } = React;
const { render } = ReactDOM;

class Parent extends Component {
  render() {
    return (
      <div>
        <Child ref={instance => { this.child = instance; }} />
        <button onClick={() => { this.child.increment(); }}>Click</button>
      </div>
    );
  }
}

class Child extends Component {
  constructor(props) {
      super(props)
    this.state = { count: 0 }
  }

  increment() {
    let { count } = this.state
    count = count + 1
    this.setState({ count })
  }

  render() {
    return (
      <div>Count: {this.state.count}</div>
    );
  }
}

render(
  <Parent />,
  document.getElementById('container')
);

The child component should be able to contain its own state, something that the parent doesn't need to know anything about, and should be able to receive an event that tells it to do something with that state.

I'm working on a component that shows sequences of data. The component handles the transition from one sequence to another. The parent object doesn't care about what the data is, or how its cycled, however it would like to be able to trigger an event where the child resets back to the first item, or move to the last item. It smells so bad to have to increment a property to tell the child to perform an action where the property isn't even used. It smells even worse if using this sequence component requires the parent to actually handle the movement through the sequence.

What is the point of the component if I'm just putting all the logic in the parent? I should just feed all the data into the child and let it do its thing and if I need to poke at it, it should understand what to do.

it is working for single element what if have more element in child component

prarabdhb commented 2 years ago

6 years later, do we have any solution for these kinds of scenarios? @mosesoak Any additional workarounds you might have found?

mrcljx commented 2 years ago

@prarabdhb Since the addition of context API in 2018 there are a various ways how you can solve this in user-space. One of them could be using an EventTarget and some custom hooks.

const EventContext = createContext({
  target: new EventTarget()
  cacheRef: { current: undefined },
  dispatch: () => undefined,
});

function MyParent() {
  const { dispatch } = useContext(EventContext);

  return <EventContext.Provider value={value}>
    <button onClick={() => dispatch(new Event("double"))} />
    <MyChild />
  </EventContext.Provider>
}

function useEvent({ eventName, replayLast }, callback) {
  const { target, cacheRef } = useContext(EventContext);

  const replayLastRef = useRef(replayLast);

  useEffect(() => {
    target.addEventListener(eventName, callback);
    if (replayLastRef.current) {
      replayLastRef.current = false;
      if (cacheRef.current) {
        callback(event);
      }
    }

    return () => {
      target.removeEventListener(eventName, callback)
    };
  }, [eventName, callback]);
}

function MyChild() {
  const [state, setState] = useState(2);

  const onDouble = useCallback(() => {
    setState(v => v * 2)
  }, []);

  useEvent("double", onDouble);

  return <div>Value: {String(state)}</div>
}

function App() {
  const target = useMemo(() => new EventTarget(), []);

  const cacheRef = useRef();

  const dispatch = useCallback((event) => {
    cacheRef.current = event;
    target.dispatchEvent(event);
  }, [target]);

  const value = useMemo(() => 
    return { target, cacheRef, dispatch });
  }, [target, cacheRef, dispatch]);

  return <MyParent />
}