Coder Social home page Coder Social logo

Comments (11)

satya164 avatar satya164 commented on May 1, 2024 1

Looks good!

Not sure the test in componentDidUpdate is really required as deleting/adding to the listener set is fast,

Guess it doesn't hurt to keep it.

we want to preserve listener order

Probably unnecessary.

from rfcs.

ericvicenti avatar ericvicenti commented on May 1, 2024

The purist in me wants to say that those sort of components are improper because they aren't actual views that appear on the screen.

But I've worked with codebases that do this heavily, and the developer ergonomics are pretty fantastic.

For us, it might look like this:

<NavigationEvents
  onWillFocus={...}
  onDidFocus={...}
  onWillBlur={...}
  onDidBlur={...}
/>

cc @satya164 who has also proposed componenty things like this.

from rfcs.

satya164 avatar satya164 commented on May 1, 2024

Yeah, components for events is pretty nice. With React.Fragment they are even better since we don't need a wrapper View anymore!

If we want to provide such a component, the one @ericvicenti suggested looks nice! It can get navigation from the context.

from rfcs.

slorber avatar slorber commented on May 1, 2024

from rfcs.

slorber avatar slorber commented on May 1, 2024

Here is a first draft implementation.

Note that I used an "internal listener" for each event, because we don't want to unsubscribe/resubscribe the real listener on every update, as the user would very likely provide an arrow function like this:

<NavigationEvents onWillFocus={() => doSomething()}/>

Yet, we probably want to support this case where the behavior of the provided listener is updated at runtime:

<NavigationEvents onWillFocus={isEnabled ? doSomething : doSomethingElse}/>

Please tell me what you think and if I should make a PR with such implementation.

import React from 'react';
import withNavigation from './withNavigation';

const EventNameToPropName = {
  willFocus: 'onWillFocus',
  didFocus: 'onDidFocus',
  willBlur: 'onWillBlur',
  onDidBlur: 'onDidBlur',
};

const EventNames = Object.keys(EventNameToPropName);

class NavigationEvents extends React.Component {
  constructor() {
    super();
    this.subscriptions = {};
    this.updateListeners();
  }

  componentDidUpdate() {
    this.updateListeners();
  }

  componentWillUnmount() {
    EventNames.forEach(this.removeListener);
  }

  updateListeners = () => {
    EventNames.forEach(eventName => {
      const propName = EventNameToPropName[eventName];
      if (this.props[propName] && !this.subscriptions[eventName]) {
        this.setupListener(eventName);
      }
      if (!this.prop[propName] && this.subscriptions[eventName]) {
        this.removeListener(eventName);
      }
    });
  };

  setupListener = eventName => {
    const propName = EventNameToPropName[eventName];
    let count = 0;
    const listener = payload => {
      // /!\ It's important to get ref to the callback inside the closure to handle callback updates
      this.props[propName](payload, count);
      count++;
    };
    this.subscriptions[eventName] = this.props.navigation.addListener(
      eventName,
      listener
    );
  };

  removeListener = eventName => {
    this.subscriptions[eventName] && this.subscriptions[eventName].remove();
  };

  render() {
    return null;
  }
}

export default withNavigation(NavigationEvents);

Note that in the callback, I provide the payload and a count. I'm not really sure of the API of this callback, maybe you have opinion on that?

What I can say is that in my app, I need the infos provided (count + payload) to implement the behavior I want: refresh a Tab on every focus, but not on initial mount.

        <NavigationEvents
          onWillFocus={(payload,count) => {
            if ( payload.action.type === NavigationActions.BACK ) {
              return; // We don't want to refresh on back so that user does not loose pagination state
            }
            if ( count > 0 ) {
              this.refreshTabData()
            }
          }}
        />

Any opinion on the callback API design?

from rfcs.

slorber avatar slorber commented on May 1, 2024

I think the counter I need above can be implemented in userland instead.

Even if it's less handy for my usecase, it would probably pollute less the api

class MyClass extends React.Component {

  handleWillFocus = (() => {
    let count = 0;
    return payload => {
            if ( payload.action.type === NavigationActions.BACK ) {
              return; // We don't want to refresh on back so that user does not loose pagination state
            }
            if ( count > 0 ) {
              this.refreshTabData()
            }
           count++;
    }
  })();

  render() {
    return (
      <NavigationEvents onWillFocus={this.handleWillFocus}/>
    )
  }
}

from rfcs.

satya164 avatar satya164 commented on May 1, 2024

In your NavigationEvents component, you're adding the listeners in constructor. AFAIK, with async rendering in react, it's not gurranteed that component will mount when constructor/willMount is called and hence willUnmount won't be called to clean up the listener. The listener might end up getting added multiple times causing a memory leak.

Note that I used an "internal listener" for each event, because we don't want to unsubscribe/resubscribe the real listener on every update

Is there any difference between re-subscribing to internal listener and real listener? In theory, resubscribing to the real listener should be of the same impact.

I provide the payload and a count

I think it's unncecessary to provide a count.

from rfcs.

slorber avatar slorber commented on May 1, 2024

In your NavigationEvents component, you're adding the listeners in constructor. AFAIK, with async rendering in react, it's not gurranteed that component will mount when constructor/willMount is called and hence willUnmount won't be called to clean up the listener. The listener might end up getting added multiple times causing a memory leak.

So you think it's better to add listeners in componentDidMount?

Is there any difference between re-subscribing to internal listener and real listener? In theory, resubscribing to the real listener should be of the same impact.

In theory, if the cost of removing/adding listener is not significant, and if I keep the same callback API, I could register directly the prop functions as listeners and make a simpler implementation.

Does anyone see any performance issue? I think it would be ok as it would only be a Set remove/add op: https://github.com/react-navigation/react-navigation/blob/8ed3817c9030f4b790f0e24e0197d8dde9b724d1/src/getChildEventSubscriber.js#L149

from rfcs.

satya164 avatar satya164 commented on May 1, 2024

So you think it's better to add listeners in componentDidMount?

Yeah, that's the recommended way.

from rfcs.

slorber avatar slorber commented on May 1, 2024

Does this look good to you?

import React from 'react';
import withNavigation from './withNavigation';

const EventNameToPropName = {
  willFocus: 'onWillFocus',
  didFocus: 'onDidFocus',
  willBlur: 'onWillBlur',
  onDidBlur: 'onDidBlur',
};

const EventNames = Object.keys(EventNameToPropName);

class NavigationEvents extends React.Component {
  constructor() {
    super();
  }

  componentDidMount() {
    this.subscriptions = {};
    EventNames.forEach(this.addListener);
  }

  componentDidUpdate(prevProps) {
    EventNames.forEach(eventName => {
      const listenerHasChanged =
        this.props[EventNameToPropName[eventName]] !==
        prevProps[EventNameToPropName[eventName]];
      if (listenerHasChanged) {
        this.removeListener(eventName);
        this.addListener(eventName);
      }
    });
  }

  componentWillUnmount() {
    EventNames.forEach(this.removeListener);
  }

  addListener = eventName => {
    const listener = this.props[EventNameToPropName[eventName]];
    if (listener) {
      this.subscriptions[eventName] = this.props.navigation.addListener(
        eventName,
        listener
      );
    }
  };

  removeListener = eventName => {
    if (this.subscriptions[eventName]) {
      this.subscriptions[eventName].remove();
      this.subscriptions[eventName] = undefined;
    }
  };

  render() {
    return null;
  }
}

export default withNavigation(NavigationEvents);

Not sure the test in componentDidUpdate is really required as deleting/adding to the listener set is fast, but maybe if there are many listeners in the set we want to preserve listener order? (this order will not be kept with arrow functions anyway so this may be quite useless?)

from rfcs.

slorber avatar slorber commented on May 1, 2024

closing as it just got merged ! :)

from rfcs.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.