Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ Initial amp-timeago Bento component #26507

Merged
merged 41 commits into from
Mar 5, 2020

Conversation

caroqliu
Copy link
Contributor

@caroqliu caroqliu commented Jan 27, 2020

No description provided.

@caroqliu caroqliu requested a review from jridgewell January 27, 2020 20:54
@caroqliu caroqliu requested a review from wassgha January 27, 2020 20:54
Copy link
Contributor

@jridgewell jridgewell left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mutatedAttributes callback is already integrated with PreactBaseElement. You should not override it. Instead, when it's received, we rerender the Component.

viewportCallback is to be handled inside the Component via an IntersectionObserver. See code like https://medium.com/the-non-traditional-developer/how-to-use-an-intersectionobserver-in-a-react-hook-9fb061ac6cb5

extensions/amp-timeago/0.2/timeago.js Outdated Show resolved Hide resolved
Copy link
Member

@samouri samouri left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A nit and some questions

extensions/amp-timeago/0.2/amp-timeago.js Show resolved Hide resolved
extensions/amp-timeago/0.2/amp-timeago.js Show resolved Hide resolved
extensions/amp-timeago/0.2/use-intersect.js Outdated Show resolved Hide resolved
extensions/amp-timeago/0.2/use-intersect.js Outdated Show resolved Hide resolved
extensions/amp-timeago/0.2/use-intersect.js Outdated Show resolved Hide resolved
extensions/amp-timeago/0.2/timeago.js Outdated Show resolved Hide resolved
extensions/amp-timeago/0.2/timeago.js Outdated Show resolved Hide resolved
extensions/amp-timeago/0.2/use-intersect.js Outdated Show resolved Hide resolved
extensions/amp-timeago/0.2/use-intersect.js Outdated Show resolved Hide resolved
extensions/amp-timeago/0.2/timeago.js Outdated Show resolved Hide resolved
@wassgha wassgha requested a review from dvoytenko January 30, 2020 19:10
src/preact/use-intersect.js Outdated Show resolved Hide resolved
src/preact/use-in-view.js Outdated Show resolved Hide resolved
src/preact/use-in-view.js Outdated Show resolved Hide resolved
src/preact/use-in-view.js Outdated Show resolved Hide resolved
src/preact/use-in-view.js Outdated Show resolved Hide resolved
src/preact/use-in-view.js Outdated Show resolved Hide resolved
src/preact/use-in-view.js Outdated Show resolved Hide resolved
src/preact/use-in-view.js Outdated Show resolved Hide resolved
extensions/amp-timeago/0.2/timeago.js Outdated Show resolved Hide resolved
extensions/amp-timeago/0.2/amp-timeago.js Show resolved Hide resolved
extensions/amp-timeago/0.2/timeago.js Outdated Show resolved Hide resolved
extensions/amp-timeago/0.2/timeago.js Outdated Show resolved Hide resolved
src/preact/use-is-intersecting.js Outdated Show resolved Hide resolved
@samouri samouri closed this Feb 25, 2020
@jridgewell jridgewell reopened this Feb 25, 2020
src/preact/use-is-intersecting.js Outdated Show resolved Hide resolved
src/preact/use-is-intersecting.js Outdated Show resolved Hide resolved
@caroqliu
Copy link
Contributor Author

caroqliu commented Feb 28, 2020

Current approach has the following tradeoff: Instead of setTimestamp on enter viewport, this approach calls fresh getFuzzyTimestampValue on every render -- this means render on exit viewport and with prop mutations

extensions/amp-timeago/0.2/timeago.js Outdated Show resolved Hide resolved
extensions/amp-timeago/0.2/timeago.js Outdated Show resolved Hide resolved
return createElement(
'time',
{datetime: props['datetime'], ref},
getFuzzyTimestampValue(props)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This changes the timestamp on each render. And per spec it should only do so when the element returns back to the viewport, right? Why not just do it in the if (isIntersecting) {} and store the value in the state instead of isIntersecting?

Copy link
Contributor Author

@caroqliu caroqliu Mar 4, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was under the impression that this approach will cost us rendering a fresh timestamp on prop mutation from amp-bind. Specifically this is the case that's important to support:
image

i.e. the following does nothing on prop mutation.

export function Timeago(props) {
  const [timestamp, setTimestamp] = useState(null);
  const ref = useRef(null);

  useEffect(() => {
    const node = ref.current;
    const observer = new IntersectionObserver(entries => {
      const last = entries[entries.length - 1];
      if (last.isIntersecting) {
        setTimestamp(getFuzzyTimestampValue(props));
      }
    });
    if (node) {
      observer.observe(node);
    }
    return () => observer.disconnect();
  }, []);

  useResourcesNotify();
  return createElement('time', {datetime: props['datetime'], ref}, timestamp);
}

We could update the timestamp on every render to handle prop mutation, but that both doesn't differentiate between the attempt to render on exit viewport and can also be consolidated into the approach we have now.

Copy link
Member

@samouri samouri Mar 4, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So,

  • you want to update ts on either prop change or onEnterViewport
  • you do not want to update ts on random parent rerender or onExitViewport.

I think you can follow Dima's suggestion, but then also have a clause like this to update on prop change:

useEffect(() => setTimestamp(...), [props.timestamp])

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but then also have a clause like this to update on prop change:

See reactjs/rfcs#107 (comment), and reactjs/rfcs#107 (comment). We should not do that.

We have two cases for intersecting rerendering now:

  1. false -> true
  2. true -> false

If we want to make it so that we only rerender when false -> true, we need to introduce a high order state helper. I had proposed something like:

function useRerenderWhenGoingTrue(initial) {
  const ref = useRef(initial);
  const [, setCount] = useState(0);

  const setter = useCallback(newValue => {
    const oldValue = ref.current;
    if (newValue === true && oldValue === false) {
      setCount((count) => count + 1);
    }
    ref.current = newValue;
  })

  return [ref.current, setter];
}

This will cause a rerender only when inIntersecting goes from false -> true:

const [isIntersecting, setIsIntersecting] = useRerenderWhenGoingTrue(false);

const inob = new IntersectionObserver(rs => {
  setIsIntersecting(rs[0].isIntersecting);
});

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But, I think the code was fine as is, it doesn't matter too much if timeago renders when exiting viewport.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rerunning effect on prop change is very common and no problem with it that I can think of. The "this pattern is shady" in the links, I believe, is with passing arrays around.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, the initial problem wasn't with the "renders when exiting viewport", but with the content changing on every external rerender with state/props being invariant.

@caroqliu caroqliu requested a review from dvoytenko March 4, 2020 21:25
Copy link
Contributor

@dvoytenko dvoytenko left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is LGTM from my side. I know this has been a very controversial topic. And perhaps we don't have to iron out all of these kinks on a single component. But the way I see it: this component does what it promises to do and it does it fairly optimally. I'd be happy to continue the discussion on this, but I think we can merge this now and adjust if we find a reason to.

observer.observe(node);
}
return () => observer.disconnect();
}, [datetime, locale, cutoff, cutoffText]);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Feel free to ignore since everyone else has approved this, but won't this create a new IntersectionObserver on any prop change? (Thats why I was thinking this should be a different useEffect)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes it will. We discussed as an optimization, we can separate side-effects like you suggested or use one side-effect + useMemo to re-calculate the value. E.g.

const [intersectionCount, setIntersectionCount] = useState(0);
const timestamp = useMemo(() => {
  if (intersectionCount < 1) {
    return null;
  }
  return getFuzzyTimestamp(datetime, locale, cutoff, cutoffText);
}, [intersectionCount, datetime, locale, cutoff, cutoffText]);

useEffect(() => {
  new IntersectionObserver(() => {
    if (isIntersecting) {
      setIntersectionCount(old => old + 1);
    }
  })
}, []);

return <div>{timestamp}</div>

I think this would work, and likely a better option vs current state. It's just that we wanted to merge the first version and continue discussion with the merged code.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool. Thanks for the explanation, makes sense to me 👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

9 participants