Rado's blog

Handling paths in React application

When using React Router, "a" components should be replaced by Link:

<Link to="/about">About</Link>  
<Link to={`/${post.categorySlug}/${post.slug}`}>{post.name}</Link>  

Passing a route to Link as string works, but doesn't protect us from mistyping. Also changing routes is not very easy. For example if we decide that /${post.categorySlug}/${post.slug} should become /posts/${post.slug}, there would be a lot of grepping.

Ruby on Rails solves those problems by generating a method for every route your application.

This concept works great with React Router:

<Link to={paths.about()}>About</Link>  
<Link to={paths.post(post)}>{post.name}</Link>  

All you have to do is define all your routes in file:

// routes.js
export default {  
   post(post) {
     return `/${post.categorySlug}/${post.slug}`;
   },

   about() {
    return '/about';
   },

   // not all routes are strings
   contacts() {
     return { pathname: 'contacts', state: { modal: true } }; 
   },

   // helper for image sources
   image(path) {
     return `https://product-hunt-cdn.com/images/${path}`
   }

   // ....
};

This technique works great with flow, giving you type safety in your links.

I have thought several times about generating this file from router component. But didn't have a chance to do so and with React Router v4 this won't be very easy.

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: 
    {| user: User, width: number, height: number |} |
    {| 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 "data-test" 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 data-test 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('[data-text="expand-button"]');
  });

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

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

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

    expect(el).not.to.have.descendants('[data-test="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 
          data-test="expand-button" 
          className={styles.expand} 
          onClick={this.expand}>show more</button>
      </div>
    );
  }

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

I'm also using data-test 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_test_attr` or `click_test_attr`
    find(:css, '[data-test="expand"]').click

    expect(page).to have_text product.description

    # This can be extracted into `have_test_arr`
    expect(page).not_to have_css('[data-test="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.