addEventListener for window resize

Hey, I’m still trying to get comfortable with the JS interop. Was wondering if anyone has an example of adding an event listener (bonus points if it listens for a window resize). I’d typically use this to build a responsive React component that switches between mobile, tablet, desktop etc. This would be a typical example of the hook that I’d use:

const useViewport = () => {
  const [width, setWidth] = React.useState(window.innerWidth);

  React.useEffect(() => {
    const handleWindowResize = () => setWidth(window.innerWidth);
    window.addEventListener("resize", handleWindowResize);
    return () => window.removeEventListener("resize", handleWindowResize);
  }, []);

  return { width };
}

Any examples that could help me in this direction would be great! :smile:

The new direct (dangerous) interop would look like:

@val external window: {..} = "window"

let reportWindowSize = _evt => {
  Js.log(window["innerWidth"])
}

window["addEventListener"]("resize", reportWindowSize)

This approach of directly invoking JS functions without explicit bindings is documented at https://rescript-lang.org/docs/manual/latest/external#tips--tricks

1 Like

Another approach with incrementally a bit more type safety by using type annotations and raw:


let getInnerWidth: unit => int = %raw(`() => window.innerWidth`)
let addWindowEventListener: (
  string,
  unit => unit,
) => unit = %raw(`(event, handler) => window.addEventListener(event, handler)`)
let removeWindowEventListener: (
  string,
  unit => unit,
) => unit = %raw(`(event, handler) => window.removeEventListener(event, handler)`)

let useViewport = () => {
  let (width, setWidth) = React.useState(getInnerWidth)

  React.useEffect0(() => {
    let handleWindowResize = () => setWidth(_ => getInnerWidth())
    addWindowEventListener("resize", handleWindowResize)

    Some(() => removeWindowEventListener("resize", handleWindowResize))
  })

  width
}

But given how simple the JS is, it is also very simple to use the @scope and @val interop and leave @raw for longer and harder JS code to bind to:

@scope("window") @val
external windowInnerWidth: int = "innerWidth"
@scope("window") @val
external addWindowEventListener: (string, unit => unit) => unit = "addEventListener"
@scope("window") @val
external removeWindowEventListener: (string, unit => unit) => unit = "removeEventListener"

let useViewport = () => {
  let (width, setWidth) = React.useState(_ => windowInnerWidth)

  React.useEffect0(() => {
    let handleWindowResize = () => setWidth(_ => windowInnerWidth)
    addWindowEventListener("resize", handleWindowResize)

    Some(() => removeWindowEventListener("resize", handleWindowResize))
  })

  width
}
1 Like

Literally just got to this last solution, almost word for word! Thanks though :smiley: :clap:

Since the reason for this post was about building a responsive builder component, I thought I’d provide my full solution :slight_smile:

Starting off, Responsive.res, this module defines the hook that listens for window resizes, and exposes a component that sends the screen width down the context.

//Responsive.res
@val @scope("window")
external addEventListener: (string, unit => unit) => unit = "addEventListener"

@val @scope("window")
external removeEventListener: (string, unit => unit) => unit = "removeEventListener"

@val @scope("window")
external innerWidth: int = "innerWidth"

let useViewport = () => {
  let (width, setWidth) = React.useState(() => innerWidth)

  React.useEffect(() => {
    let handleWindowResize = () => {
      setWidth(_ => innerWidth)
    }

    addEventListener("resize", handleWindowResize)

    let cleanUp = () => removeEventListener("resize", handleWindowResize)

    Some(cleanUp)
  })

  width
}

let context = React.createContext(0)

module WidthProvider = {
  let provider = React.Context.provider(context)

  @react.component
  let make = (~value, ~children) => {
    React.createElement(provider, {"value": value, "children": children})
  }
}

module Provider = {
  @react.component
  let make = (~children: React.element) => {
    let width = useViewport()

    <WidthProvider value=width> children </WidthProvider>
  }
}

let provider = Provider.make

My .resi file looks like this:

//Responsive.resi
let provider: {"children": React.element} => React.element
let context: React.Context.t<int>

only exposing the context and the Provider.make (this may seem strange but will show why soon, of course you can expose as much as you want here).

Then I have a ResponsiveBuilder component:

//ResponsiveBuilder.res
open Belt.Option
@react.component
let make = (
  ~xs: React.element,
  ~sm: option<React.element>=?,
  ~md: option<React.element>=?,
  ~lg: option<React.element>=?,
  ~xl: option<React.element>=?,
) => {
  let width = React.useContext(Responsive.context)

  if width >= 1920 && xl->isSome {
    xl->getExn
  } else if width >= 1280 && lg->isSome {
    lg->getExn
  } else if width >= 960 && md->isSome {
    md->getExn
  } else if width >= 600 && sm->isSome {
    sm->getExn
  } else {
    xs
  }
}

This component is based on the Material-UI concept of “mobile-first”. This means you have to pass in the xs component, but everything after that is optional. Not sure if there are some cleaner ways to write this piece of logic? Eg. pattern matching or without the getExn?

Finally, I have a ProviderWrapper component that I use at the top level of my app:

//ProviderWrapper.res
@react.component
let make = (~providers: list<React.component<{"children": React.element}>>, ~children) => {
  providers->reduce(children, (acc, elem) => {
    React.createElement(elem, {"children": acc})
  })
}

So, putting this all together…

At the top level of my app I wrap all the global providers (generally reducers passed down context):

//App.res
let providers = list{Todo.provider, User.provider, Responsive.provider}
<ProviderWrapper providers>
    <HomeView />
</ProviderWrapper>

And then you can use the ResponsiveBuilder anywhere in your app:

//HomeView.res
<ResponsiveBuilder
      xs={"Extra small"->React.string}
      md={"Medium"->React.string}
      lg={"Large"->React.string}
      xl={"Extra Large"->React.string}
/>

Hope someone found this useful! Please let me know about any improvements I can make :smile:

1 Like