useIframeBuster React Hook

React Hooks are awesome. I know there’s a lot of criticism levelled against it recently: it’s a bit hard to grok its mental model and so on, but I fell in love with them the day they were introduced. For one simple reason: it’s now possible to compose side-effects the same way you could compose components earlier. The community can build & share these hooks now, and it’s less code that you have to write. So here’s the story of how I extracted out a generic useIframeBuster hook for when you have to bust out of iframes.

Step 1 is to Google

How to bust out of iframes” literally which leads you to this piece of code:


if (top.location!= self.location) {
   top.location = self.location.href;
}

Convert this to a Hook

useEffect comes to the rescue here:


import { useEffect } from "react";

const useIframeBuster = () => {
  useEffect(() => {
    const script = document.createElement("script");
    script.innerHTML = `
      (function () {
        'use strict';
        if (window.location !== window.top.location) {
            window.top.location = window.location;
        }
    })();`;
    script.async = true;

    document.body.appendChild(script);

    return () => {
      document.body.removeChild(script);
    };
  }, []);
};

export default useIframeBuster;

It’s pretty simple code:

  1. useEffect is used with a [] as a parameter which means this will run only on first component render.
  2. And when it does, it executes the code we found via googling! There’s a bit of manual element creation here, but that’s what useEffect is for: it isolates these side-effects from our regular React component model.
  3. To use this effect, a component just needs to import useIframeBuster, and then do a simple useIframeBuster() somewhere in its exported function. Simple, and all the ugly details are wrapped in this easily testable function.

Think of the UX

We can pause here and think this is compete, but let’s think of what happens to a page that has this effect loaded. It’ll render completely once, then bust out and then render again once the script does its job and the bust out is complete. That’s very bad UX. We should add a simple Redirect component:


import React, { useEffect } from "react";
import useIframeBuster from "./effects/useIframeBuster";
import { useLocation } from "react-router-dom";
import isValidUrl from "isValidUrl";

const RedirectWithBustOut = () => {
  useIframeBuster();
  let location = useLocation();
  let path = new URLSearchParams(location.search).get("path") ?? "";

  /**
   * If path is a valid URL, we set path to "/" to avoid
   * Open Redirection Vulnerability, see:
   * https://dzone.com/articles/what-is-an-open-redirection-vulnerability-and-how
   */
  if (isValidUrl(path)) {
    path = "/";
  }

  useEffect(() => {
    setTimeout(() => {
      window.location.href = `${path}`;
    }, 1000);
  }, []);

  return (
    <>
      <p>Loading...</p>
    </>
  );
};

export default RedirectWithBustOut;

We can then invoke this component like so:


<RedirectWithBustOut path="/index" />

& the user will see a Loading... indicator for a while before the page busts out and the path passed in renders. Notice some details:

  1. There is a wrapped setTimeout() that waits a second (you can reduce this time) to make sure the page fully loads before the path is changed. This gives our useIframeBuster enough time to bust out of the page.
  2. There is a possible Open Redirection vulnerability here that we have to mitigate.

As you can see it’s pretty simple code, and we’ve built out an abstraction that you can now reuse hundreds of times. Good stuff!

Thoughts about destiny: the prettier for Filesystems

I recently encountered destiny on Twitter, and it’s one of those ideas that instantly appeal to you: what prettier did to code, destiny does to filesystems. I tried this out on my js-rockstars project, and it’s a solid idea. Here’s before and after destiny worked its magic on the filesystem:

It still needs work (it’s very early days), but before I get into that, this is why I believe something like destiny is the future:

  1. Programming is a cognitively difficult job. It takes a lot to actually produce working, elegant code that both does the job and is intelligible to humans. Part of the reason why prettier succeeded I believe, is that it takes away those decisions that doesn’t matter away from you into the hands of a collaboratively agreed upon ideal.
  2. Programmers are nitpickers. We have to be, because we work with a computer that’s the grand daddy of all nitpickers. But if we are let to our own devices, we optimise those things that doesn’t need to be optimised.
  3. All things organisation (& this includes code formatting and filesystem organisation) should be a set of declarative rules that you configure once and then it’s automatically executed by a rule engine. You decide how the organisation should be (because you do that best), and a computer does the actual work (because that’s what it does best, take orders). This is how you make the computer work for you. Or in other words, augmented intelligence.

Now, here’s 3 things I believe can be improved[1]:

  1. Ben has a pretty great video behind the motivation for destiny. It’s a nice watch, but it approaches things almost entirely from the algorithmic angle. Here’s a problem I have, here’s how I wrote an algorithm to solve it. It’s a great solution btw, using trees to model import hierarchy. This is however, not how prettier evolved: it also has a very fancy parser behind it, but the rules were derived from the community. So what destiny needs now is a community-contributed consensus-based rule engine. There is already for e.g. a -S option that avoids single-file folders, but this really needs to expand further.
  2. A “one filesystem organization method to rule them all” strategy probably wouldn’t work. Everybody has got to have their own opinion. Prettier for e.g. has a recommended configuration, but everybody can feel free to tweak this for themselves. A slight niggle when I started working with destiny was that perhaps different top-level folders might also require their own configuration. For e.g, when I ran destiny on the entire src/ folder for js-rockstars, it threw off my entire Storybook stories organization. So I decided to carve off my components/ separately and run destiny just on that. Similarly cypress/ will probably require a different set of defaults. So my current thinking around this is to somehow automagically detect what kind of folder destiny is working with, and apply the best community-created rules for that folder. This is a more nested problem than what prettier had to solve, but it’s worth solving!
  3. Using something like cosmicconfig is an easy first step here: destiny needs to grow beyond its command-line arguments, and cosmic provides the needed footing!

That’s it! I hope the community rallies around destiny or something like it, and us programmers have less silly decisions to take in the future! 🎉


[1] I’ve always believed that unsolicited feedback in open source is not enough. If you talk the talk, you have to code the code. (this came out better in my head). I look forward to contributing to destiny sometime in the near future.

JS Rockstars: Make everybody play well together!

I’ve built a small Typescript project that makes a bunch of Javascript libraries play well together. It looks like this:

I used to be a 100% Ruby guy, but I’ve learnt a lot about JavaScript and the Typescript world in the last year. A lot of the experience is great, and frankly, far better than you’d find in the Ruby world, but one thing that is clearly inferior is how many choices there are, and how all of them are “much too big to play well together”[1]. This is an attempt to fix that, an opinionated project that uses the best JS libraries out there to build out a full stack application.

You can see more info about it here: js-rockstars. Do give it a whirl and let me know feedback! 🙂


[1]: Much like one of my favourite NBA teams from the early 2000s.

A Plug for Razzle & Next

My last post (a year ago, sigh) was about how to configure a JavaScript application from scratch. Since then, I’ve built an isomorphic Node and React app that’s doing pretty well in production and learnt a lot of lessons along the way.

If you are looking for the “Rails” of the JS world, I have two suggestions for you:

  • Razzle: more configurable SSR solution that comes with most batteries included!
  • Next: lovely DX, super-fast defaults, and amazing traction (they have a collaboration with Google), but a bit less customisable.

My app was built in Razzle, but if I was to start today, I’d probably choose Next instead!

Mobshare.in Broadcast Widget

Mobshare.in Broadcast Widget

Here’s a sneak peek of the Mobshare broadcast widget I’ve been working on. If you look to the right and below, you can see that in action live. You’ll see the last ten pics I’ve uploaded and direct from the widget you can subscribe to my feed or send me an SMS.

The widget is the first released code which uses our internal API. While the API itself is undocumented [which hopefully will soon change], it’s public and it’s JSON (which makes implementing Javascript widgets incredibly easy).

How do you get your own widget? You can’t do that easily right now (the pleasures of cutting-edge code eh?), but paste this code somewhere:


<iframe width="150" height="150" src="http://mobshare.in/user/:mobshare_id:/broadcast/widget"></iframe>

…and change :mobshare_id: to your Mobshare ID.

Try it out and let me know if you like it!

An Orkut Application via a JSON API

I talked about delegating rendering in Symfony for creating a JSON API. Now here’s a consumer: an Orkut opensocial gadget:


MobshareOrkutAPI = {

	api_url: 'http://api.mobshare/api.php',
	cache_time: 0, //0 to disable

	makeCachedRequest: function(url, callback, params, refreshInterval) {
	  var ts = new Date().getTime();
	  var sep = "?";
	  if (refreshInterval && refreshInterval > 0) {
	    ts = Math.floor(ts / (refreshInterval * 1000));
	  }
	  if (url.indexOf("?") > -1) {
	    sep = "&";
	  }
	  url = [ url, sep, "nocache=", ts ].join("");
	  gadgets.io.makeRequest(url, callback, params);
	},

	call: function(module, action, params, callback) {
		var options = {};
		options[gadgets.io.RequestParameters.CONTENT_TYPE] = gadgets.io.ContentType.JSON;
		this.makeCachedRequest(this.api_url + '/' + module + '/' + action + '?' + params, callback, options, this.cache_time);
	}

};

makeCachedRequest has been plaigarized from the Opensocial documentation and it’s very useful to bust the cache for requests. Also, notice this line for setting the content-type to JSON:


options[gadgets.io.RequestParameters.CONTENT_TYPE] = gadgets.io.ContentType.JSON;

This is how we access that API, a code fragment:


...
authenticate: function(alias, mobile_no, password) {
	MobshareOrkutAPI.call('user', 'authenticate',
		'alias=' + encodeURIComponent(alias) + '&mobile_no=' + encodeURIComponent(mobile_no) +
			'&password=' + encodeURIComponent(password),
 		MyOrkutApp.login
	);
}
...
login: function(orkut_response) {
	response = orkut_response.data;
	data_success = response['success'];
	data_error = response['error'];

	if(data_success) {
		html = '<h2>Login Successful!</h2>';
		html += '<p>Welcome: ' + data_success.name + '!</p>';
	} else if(data_error) {
		html = '<h2>Login Unsuccessful!</h2>';
		html += '<p>' + data_error + '!</p>';
	}

	document.getElementById('mobshare_login').innerHTML += html;
},
...

Note that orkut_response.data is automatically set by Orkut because you passed in the JSON content type; it parses the data received and creates a javascript object. Cool, ain’t it? It’s very easy to create a proper interactive Opensocial app this way.

Self-labeling text entry INPUT boxes using Prototype JS

Like previous code snippets, this one is mostly all code and no explanation.

A brief primer: you’ve often seen self-labeling input boxes, the ones that display a label inside in gray text, and when you click replaces with an empty input element using Javascript. This is a simple class to help you achieve that effect:

SelfLabelingBox = {
	initialize: function(box_id, message) {
		this.setup_behavior(box_id, message);
	},

	setup_behavior: function(box_id, message) {
		box_id = $(box_id);
		if(box_id) {
			if(box_id.value == '') {
				box_id.value = message;
				box_id.addClassName('inactive');
			}
			Event.observe(box_id, 'focus', function() {
				if(box_id.value == message) {
					box_id.value = '';
					box_id.removeClassName('inactive');
				}
			});
			Event.observe(box_id, 'blur', function() {
				if(box_id.value == '') {
					box_id.addClassName('inactive');
					box_id.value = message;
				}
			});
		}
	}
};

SetupDefaultInputs = {
	initialize: function() {
		Event.observe(document, 'dom:loaded', this.setup_default_inputs);
	},

	setup_default_inputs: function() {
		SelfLabelingBox.initialize('search_text', 'Search');
	}
};

Until next time!