Docs / ReasonReact / SubscriptionsHelper

Subscriptions Helper

In a large, heterogeneous app, you might often have legacy or interop data sources that come from outside of the React/ReasonReact tree, or a timer, or some browser event handling. You'd listen and react to these changes by, say, updating the state.

For example, Here's what you're probably doing currently, for setting up a timer event:

type state = { timerId: ref(option(Js.Global.intervalId)) }; let component = ReasonReact.reducerComponent("Todo"); let make = _children => { ...component, initialState: () => {timerId: ref(None)}, didMount: self => { self.state.timerId := Some(Js.Global.setInterval(() => Js.log("hello!"), 1000)); }, willUnmount: self => { switch (self.state.timerId^) { | Some(id) => Js.Global.clearInterval(id); | None => () } }, render: /* ... */ };

Notice a few things:

  • This is rather boilerplate-y.

  • Did you use a ref(option(foo)) type correctly instead of a mutable field, as indicated by the Instance Variables section?

  • Did you remember to free your timer subscription in willUnmount?

For the last point, go search your codebase and see how many setInterval you have compared to the amount of clearInterval! We bet the ratio isn't 1 =). Likewise for addEventListener vs removeEventListener.

To solve the above problems and to codify a good practice, ReasonReact provides a helper field in self, called onUnmount. It asks for a callback of type unit => unit in which you free your subscriptions. It'll be called before the component unmounts.

Here's the previous example rewritten:

let component = ReasonReact.statelessComponent("Todo"); let make = _children => { ...component, didMount: self => { let intervalId = Js.Global.setInterval(() => Js.log("hello!"), 1000); self.onUnmount(() => Js.Global.clearInterval(intervalId)); }, render: /* ... */ };

Now you won't ever forget to clear your timer!

Design Decisions

Why not just put some logic in the willUnmount lifecycle? Definitely do, whenever you could. But sometimes, folks forget to release their subscriptions inside callbacks:

let make = _children => { ...component, reducer: (action, state) => { switch (action) { | Click => ReasonReact.SideEffects(self => { fetchAsyncData(result => { self.send(Data(result)) }); }) } }, render: /* ... */ };

If the component unmounts and then the fetchAsyncData calls the callback, it'll accidentally call self.send. Using let cancel = fetchAsyncData(...); self.onUnmount(() => cancel()) is much easier.

Note: this is an interop helper. This isn't meant to be used as a shiny first-class feature for e.g. adding more flux stores into your app (for that purpose, please use our local reducer). Every time you use self.onUnmount, consider it as a simple, pragmatic and performant way to talk to the existing world.