Rado's blog

Flow as replacement for PropType

A couple of months ago at Product Hunt, we decided to switch from React.PropTypes to Flow. Initially, we started using Flow just for function definitions. Then we start replacing propType.

Why we do did that? Let's say we have an UserImage component with two possible uses:

<UserImage user={user} width={50} height={30} />  
<UserImage user={user} variant="small" />  

Its definition would be something like this:

const UserShape = {  
  id: React.PropTypes.number.isRequired,
  name: React.PropTypes.name.isRequired,
};

class UserImage extends React.Component {  
  static propTypes = {
    user: React.PropTypes.shape(UserShape).isRequired,
    width: React.PropTypes.number,
    height: React.PropTypes.number,
    variant: React.PropTypes.oneOf(['big', 'medium', 'small')
  };

  render() { /* ... */ }
}

There are some problems with that definition:

  • UserShape can be used only with other React components
  • propTypes throws a warning during runtime
  • width and height should be passed together, but we can't enforce that
  • It is possible to pass all three props - width, height, variant

Here is how the Flow solves those problems:

type User = {  
  id: number,
  name: string,
};

class UserImage extends React.Component {  
  props: {
    user: User,
    width: number,
    height: number,
  } | {
    user: User,
    variant: 'big' | 'medium' | 'small',
  };

  render() { /* ... */ }
}
  • User is a generic type (can be used for any javascript function)
  • Flow types are used only during build
  • <UserImage user={user} width="10" /> breaks the build 😎

Unfortunately this is still possible:

<UserImage user={user} width={50} height={30} variant="small" />  

One way to solve that is the following:

  props: {
    user: User,
    width: number,
    height: number,
    // expects "variant" is not passed
    variant?: void,
  } | {
    user: User,
    variant: 'big' | 'medium' | 'small',
    // expects "width" and "height" are not passed
    width?: void,
    height?: void,
  };

It's a bit ugly, but it gets the job done:

<UserImage user={user} width={50} height={30} variant="small" />  
                       ^^^^^^^ number. This type is incompatible with void

<UserImage user={user} width={50} height={30} variant="small" />  
                                   ^^^^^^^ number. This type is incompatible with void

<UserImage user={user} width={50} height={30} variant="small" />  
                                                       ^^^^^^^ string. This type is incompatible with void

For more information check Flow documentation.

p.s. Gabriele Petronella and Vjeux told me on twitter about $Exact and {| |}:

  props: 
    $Exact<{user: User, width: number, height: number}> |
    $Exact<{user: User, variant: 'big' | 'medium' | 'small'}>;

Rails 5 features, I'm excited about

Ruby on Rails 5 has arrived. Though, I don't care much about the big features. I'm more excited about the smaller features, which would not change my life, but will make it a bit easier.

API mode improvements

Most of my work in Rails projects, these days is just using Rails as an api endpoint. There are lots of improvements.

I'm really glad we finally got ActionController::API.

Easier conversion of errors to JSON

Returning consistent and usable error messages is vital when designing and API.

Previously getting usable validation errors from rails was quite painful. Since you can only have:

user.errors.messages # => {:email=>["can't be blank"]}  

This error message is it quite hard for clients. In several projects, I have defined a json locale where things like "can't be blank" is just "blank" and used that for the api.

But this is no more! Since errors.details exists now:

user.errors.details # => {:email=>[{:error=>:blank}]}  
Better exceptions in development

Speaking about errors. There is a new option debugexceptionresponse_format:

# config/environments/development.rb
config.debug_exception_response_format = :api  

You will get exception information in JSON format during development, instead of HTML.

ActiveJob improvements

In a previous post - Retrying ActiveJob, there was a glue code depending on a Rails 5 feature - ActiveJob::Base#deserialize. Now that can be removed.

Also having the ApplicationJob makes including the ActiveJobRetriesCount from the post easier:

class ApplicationJob < ActiveJob::Base  
  include ActiveJobRetriesCount
end  

ActiveRecord improvements

And even more

There is, even more, stuff like redirect_back or ActiveModel::AttributeAssignment. But for more information I encourage you to check:

Overall I think this is a solid release.

I have heard on The Bike Shed that there are discussions about changing the release process and to smaller and more frequent release cycle. I think this would be great. Especially since the features, I'm mostly excited usually are smaller and don't require a major version change.

Using "rel" in testing

When testing HTML components, I often see people using class names as selectors. For example:

element.find('.description button.expand-button').simulate('click');  

While this seems convenient at first, there are some drawbacks. HTML structure and css classes tend to change due to design changes. Which will cause you re-write tests quite often. Also, if you are using css-modules you can't rely on class names.

Because of that, for quite some time now, I have started marking elements with rel attribute.

React example (using enzyme and chai-enzyme):

describe(Description.name, () => {  
  it('cut off text based on `cutoffLength`', () => {
    const el = shallow(<Description text="test" cutoffLength={1} />);

    expect(el).to.have.text('t...');
    expect(el).not.to.have.text('test');
  });

  it('hides expand button when text is short', () => {
    const el = shallow(<Description text="test" cutoffLength={10} />);
    expect(el).not.to.have.descendants('[rel="expand-button"]');
  });

  it('shows expand button when text is long', () => {
    const el = shallow(<Description text="test" cutoffLength={1} />);
    expect(el).to.have.descendants('[rel="expand-button"]');
  });

  it('clicking expand button reveals the whole text', () => {
    const el = shallow(<Description text="test" cutoffLength={1} />);

    el.find('[rel="expand-button"]').simulate('click');

    expect(el).not.to.have.descendants('[rel="expand-button"]');
    expect(el).to.have.text('test');
  });
});

The component code:

import React from 'react';  
import styles from "./style.css";

export default Description extends React.Component {  
  state = { expanded: false };

  render() {
    const { text, cutoffLength } = this.props;

    if (this.state.expanded || text.length < cutoffLength) {
      return (
        <div className={styles.description}>
          {this.props.text}
        </div>
      );
    }

    return (
      <div className={styles.description}>
        {`${ text.substr(0, cutoffLength) }...`}
        <button 
          rel="expand-button" 
          className={styles.expand} 
          onClick={this.expand}>show more</button>
      </div>
    );
  }

  expand = () => {
    this.setState({ expanded: true });
  };
}

I'm also using rel attributes for testing with Capybara in Ruby land.

describe 'Product page' do  
  it 'has product description rev' do
    product = create :post, :with_long_description

    visit product_path(product)

    expect(page).not_to have_text product.description

    # This can be extracted into `find_rel` or `click_rel`
    find(:css, '[rel="expand"]').click

    expect(page).to have_text product.description

    # This can be extract into `have_rel`
    expect(page).not_to have_css('[rel="expand"]')
  end
end  

Retrying ActiveJob

I like ActiveJob. The one missing feature is the ability to retry when a job fails. Fortunately, this is quite easy to add:

module ActiveJobRetriesCount  
  extend ActiveSupport::Concern

  included do
    attr_accessor :retries_count
  end

  def initialize(*arguments)
    super
    @retries_count ||= 0
  end

  def deserialize(job_data)
    super
    @retries_count = job_data['retries_count'] || 0
  end

  def serialize
    super.merge('retries_count' => retries_count || 0)
  end

  def retry_job(options)
    @retries_count = (retries_count || 0) + 1
    super(options)
  end
end  
class ImageDownloader::Worker < ActiveJob::Base  
  include ActiveJobRetriesCount

  rescue_from ImageDownloader::TimeoutError do |exception|
    if exception.code == 0
      # just retry the download in 5 minutes
      # tolarate up to 5 failures
      retry_job wait: 5.minutes if retries_count > 5
    else
      fail exception
    end
  end

  def perform(url)
    ImageDownloader.download(url)
  end
end  

Here is a simple test for the ActiveJobRetriesCount.

require 'spec_helper'

describe ActiveJobRetriesCount do  
  class RetriesTestClass
  end

  class RetriesTestError < StandardError
  end

  class RetriesTestWorker < ActiveJob::Base
    include ActiveJobRetriesCount

    rescue_from RetriesTestError do
      retry_job wait: 5.minutes if retries_count < 5
    end

    def perform
      RetriesTestClass.do_something(retries_count)
    end
  end

  it 'handles retries counts', active_job: :inline do
    allow(RetriesTestClass).to receive(:do_something).and_raise RetriesTestError

    expect { RetriesTestWorker.perform_later }.not_to raise_error

    expect(RetriesTestClass).to have_received(:do_something).with(5)
    expect(RetriesTestClass).not_to have_received(:do_something).with(6)
  end
end  

If you are not on Rails 5, you would need the following code:

if ActiveJob::Base.method_defined?(:deserialize)  
  fail 'This no longer needed.'
else  
  module ActiveJob
    class Base
      def self.deserialize(job_data)
        job = job_data['job_class'].constantize.new
        job.deserialize(job_data)
        job
      end

      def deserialize(job_data)
        self.job_id               = job_data['job_id']
        self.queue_name           = job_data['queue_name']
        self.serialized_arguments = job_data['arguments']
      end
    end
  end
end  

For more advanced features – check out ActiveJob::Retry.

Custom routes in Rails

In the life of almost all Rails applications, comes the moment where custom routes are needed. Usually, the custom routes are just convenience methods like these:

def post_path(post)  
  date_post_path post.date_slug, post.slug
end

def product_url  
  date_post_url post.date_slug, post.slug
end  

In this way, you decouple the route generation from your objects. Also, if you are doing some system route update and you want to keep the old routes around - this is the way.

Usually, I create a module CustomPaths and put those methods there. But in recent years, Rails.application.routes.url_helpers is showing up in more and more places like - background jobs, presenters, serializers and so. This complicates things.

At Product Hunt we needed some custom paths. So, me and Mike Coutermarsh created the following object:

# => routes.rb
module Routes  
  # Simple `extend self` won't work here,
  # due to the way Rails implement `url_helpers`
  class << self
    include Rails.application.routes.url_helpers
    include Routes::CustomPaths
  end

  include Rails.application.routes.url_helpers
  include Routes::CustomPaths

  def default_url_options
    Rails.application.routes.default_url_options
  end
end

# => routes/custom_paths.rb
module Routes  
  module CustomPaths
    # ... all your custom route methods
  end
end  

It behaves the same way as Rails.application.routes.url_helpers plus the custom routes.

# in can be called directly
Routes.root_path  
Routes.product_path(product)

# or included in another object
class ObjectWhoNeedsRoutes  
  include Routes
end

ObjectWhoNeedsRoutes.new.root_path  
ObjectWhoNeedsRoutes.new.product_path  

I added Routes to my toolbox with install instructions and tests.