Author: Sherry

chic classy developer (;

React Best Practices

This is a living doc!

Based on the following:

  • Babel/ ES6+
  • Immutable
  • React 15.4
  • Recompose
  • Redux
  • Reselect
  • ESLint: Airbnb JS/React Standards

Menu:

  • Component
  • Redux
  • Reselect
  • Recompose
  • Others (naming)
  • Going forward…

Component

Component

  • Comes with lifecycle methods (see below)
  • Use when the component renders based on state

PureComponent

  • Similar to Component, but its shouldComponentUpdate does shallow comparison on state change – increase performance!
  • Use only when the component state is flat and child components are also pure

Stateless function

  • Use when a component depends only on props
    • Not a React component instance

LifeCycle

  • Avoid using life cycle methods if possible; can this be a stateless component?

componentWillMount

  • Handle/ Set up props for rendering

componentDidMount

  • Make API calls

componentWillReceiveProps

  • Handle prop updates
  • Make additional API calls if necessary
  • Update Redux state

shouldComponentUpdate

  • Reduce component renders based on required props

componentDidUpdate

  • Perform actions after Render
  • Usually applies to third party libraries

componentWillUnmount

  • Cancel any outgoing network requests
  • Clear related Redux state
  • Remove event listeners

Render

  • Keep business logic outside of render
    • should be presentational content only
  • Keep render as slim as possible
    • Is your render “too smart”? Is it doing “too much”?
  • Avoid passing new closures to subcomponents: every time the parent component renders, a new function is created and passed to the input, which causes the subcomponent to re-render as well

 

More Reading Material:

 

Redux

  • Should store the minimal possible state
  • Allow Selectors to compute derived data

Actions

  • Handle side effects (external services, async code)

Reducers

  • Must be pure

Containers

  • Read a store’s data through selectors

 

Reselect

  • Use Selectors to compute derived data (esp for the UI)
  • Selectors should be co-located with their reducers
  • Selectors can be composed of other selectors

 

Recompose

  • Helpers for creating higher order components esp for optimizations
  • For memoization
  • Create HOC for:
    • Conditionals get out of control within render
    • Code reuse, logic and bootstrap abstraction
    • Render Highjacking
    • State abstraction and manipulation
    • Props manipulation

 

Others

Structure and Naming

  • Action creators: <verb><Noun>
    • request<Noun> for requesting API only
      • request<Noun>Failure
      • request<Noun>Success
    • set<Noun> for setting data only
  • Event handlers: handle<Noun>
    • Component’s event handler is on<Noun>, so not to confuse with component’s event handler, custom event handler functions uses handle<Noun>
  • Selectors: TBD

Destructure

  • Always destructure (this.props) in render unless it’s a simple, straightforward, few-liner component
  • Destructure shallowly so it is easy to read and understand where the variable is coming from

 

More Reading Materials:

https://engineering.musefind.com/our-best-practices-for-writing-react-components-dec3eb5c3fc8

https://amido.com/blog/using-es6-destructuring-in-your-react-components/

 

Going Forward… TBD

Naming

  • Selectors: name each selector as get<Noun>

Reducers

  • Build your reducers using redux-actions’ handleActions()
    • This keeps a clean switch-like syntax, while adding block scoping to the cases.
    • This means you can reuse variable of the names in each “case”. With switch, your cases are scoped to the switch, so you are forced to use var or unique names.

Thunk to Saga

Advertisements

AngularJS, Pact, and Jasmine

  1. Follow the steps in pact-consumer-js-dsl (yes, with the gem ‘pact-mock_service’)
  2. Here is where the important stuff comes with AngularJS:

given service:

angular.module('ng-cherie.services.current-theme', [
    'ng-cherie.constants'
])
    .factory('currentThemeService', function currentThemeService($http, API) {
        var service = {
            getCurrentTheme: getCurrentTheme
        };

        function getCurrentTheme() {
            var theme;
            return $http.get(API.BASE_PATH + API.CURRENT_THEME)
                .then(function getCurrentThemeSuccess(response) {
                    theme = response.data;

                    return theme;
                }, function() {
                    return null;
                });
        }

        return service;
    });

then pact with ngMidwayTester

/* globals ngMidwayTester */ 
/* globals Pact */
describe('currentThemeService', function() {
    var API,
        currentThemeProvider,
        currentThemeService,
        mockCurrentTheme,
        $httpBackend,
        tester;

    beforeEach(function() {
        angular.module('currentThemeServiceSpec', [
          'ng-cherie.services.current-theme',
          'ng-cherie.constants',
          'ng-cherie.mocks.current-theme',
          'ngMockE2E'
        ]);

        tester = ngMidwayTester('currentThemeServiceSpec');
        currentThemeService = tester.inject('currentThemeService');
        API = tester.inject('API');
        mockCurrentTheme = tester.inject('mockCurrentTheme');

        $httpBackend = tester.inject('$httpBackend');
        $httpBackend.whenGET(/\/api\/v1\/currenttheme/).passThrough();

        currentThemeProvider = Pact.mockService({
            consumer: 'ng-cherie.services.current-theme',
            provider: 'CherieApp',
            port: 1234,
            done: function (error) {
                expect(error).toBe(null);
            }
        });
    });

    afterEach(function () {
        tester.destroy();
        tester = null;
    });

    describe('getCurrentTheme', function() {
       it('should return the current theme via pact', function(done) {

           API.BASE_PATH = "http://localhost:1234" + API.BASE_PATH;

           currentThemeProvider
               .uponReceiving("a request for the current theme")
               .withRequest("get", "/api/v1/currenttheme")
               .willRespondWith(200, { "Content-Type": "application/json" }, mockCurrentTheme);

           currentThemeProvider.run(done, function(runComplete) {
               currentThemeService.getCurrentTheme().then(function(response) {
                   expect(response).toEqual(mockCurrentTheme);
                   runComplete();
               });
           });
       });
    });

});

line 12: the key is to set up a new module that contains ngMockE2E so we can use `passthrough`
line 45: make sure to replace the domain with the pact server location, in this case, it is localhost:1234.

AngularJS compile vs templateUrl

Hello, hello, it has been a while since my last post of ‘given, then, show’. If you are new to here, this is not a recommendation on how to implement, but rather just a show and tell of what you can do.

Given:

– Implement a directive that handles these providers:
– Provider A, B, C, and D share the same template
– Provider A and B have different description
– Provider C and D have no description
– Provider E and F have different templates and different description

Then:

– Assuming that templates and provider descriptions are dumb aka not stored in controllers
– Implement a directive that shows the default template, which is share across the similar providers (A, B, C, D) and a separate template per unique provider E and F.
– Implement a directive just for passing in unique descriptions

————————————>>>>>>————————————
Call the directive

<demo provider="provider"></demo>

Method 1a: use compiler


angular.module('ng-cherie.directives.demo.directive', [])
    .directive('demo', function demoDirective($compile, $templateCache) {
        return {
            bindToController: true,
            controller: 'DemoCtrl as demoCtrl',
            link: function demoLink(scope, elem, attrs, demoCtrl) {
                var defaultTemplatePath = 'app/directives/demo/templates/default.tpl.html',
                    templatePath = defaultTemplatePath.replace(/default/, demoCtrl.provider.id),
                    template;

                template = $templateCache.get(templatePath) || $templateCache.get(defaultTemplatePath);
                $compile(elem.html(template).contents())(scope);
            },
            scope: {
                provider: '='
            }
        };
    });

Method 1b: use compiler

angular.module('ng-cherie.directives.demo.directive', [])
    .directive('demo', function demoDirective($compile, $templateCache) {
        return {
            bindToController: true,
            controller: 'DemoCtrl as demoCtrl',
            link: function demoLink(scope, elem, attrs, demoCtrl) {
                var defaultTemplatePath = 'app/directives/demo/templates/default.tpl.html',
                    templatePath = defaultTemplatePath.replace(/default/, demoCtrl.provider.id),
                    template;

                template = $templateCache.get(templatePath) || $templateCache.get(defaultTemplatePath);
                elem.append($compile(template)(scope));
            },
            scope: {
                provider: '='
            }
        };
    });

Method 2: use ng-include

angular.module('ng-cherie.directives.demo.directive', [])
    .directive('demo', function demoDirective() {
        return {
            bindToController: true,
            controller: 'DemoCtrl as demoCtrl',
            scope: {
                provider: '='
            },
            templateUrl: 'app/directives/demo/templates/demo.tpl.html'
        };
    });

where app/directives/demo/templates/demo.tpl.html is

<ng-include src="demoCtrl.showTemplate()"></ng-include>

where showTemplate() determines which template to show depending on the provider (similar to demoLink())

————————————>>>>>>————————————
in default.tpl.html:
use a directive (demoDescription directive) to pass in the description and a control to show the description, so that when demo directive (see above) renders the default template directly without the description, it won’t fail on missing transclusion.

<p class="demo-description" ng-transclude ng-if="demoDescriptionCtrl.isTemplateUsedWithDirective"></p>

call the directive:

<demo-description>
    {{ 'Provider description here.' | translate }}
</demo-description>

————————————>>>>>>————————————
jasmine test

describe('Demo directive', function() {
    var _,
        $compile,
        element,
        providerMockData,
        scope;

    beforeEach(module('ng-cherie.directives.demo'));
    beforeEach(module('ng-cherie.mocks.provider'));

    beforeEach(inject(function($injector) {
        _ = $injector.get('_');
        $compile = $injector.get('$compile');
        scope = $injector.get('$rootScope').$new();
        providerMockData = $injector.get('providerMockData');
    }));

    beforeEach(function() {
        scope.provider = _.first(providerMockData.providers);
    });

    function compileDirective(scope) {
        var compiledElement = $compile('<demo provider="provider"></demo>')(scope);

        scope.$digest();

        return compiledElement;
    }

    describe('compiled markup', function() {
        it('contains the expected markup', function() {
            element = compileDirective(scope);

            expect(element[0].querySelector('.demo-body')).not.toBeNull();
        });

        //some other tests here... mostly expecting certain classes to be or not be there

        it('shows the default template when the provider is unknown', function () {
            scope.provider.id = "unexpected_provider";
            element = compileDirective(scope);

            expect(element.find('demo-description').length).toBe(0);
        });
    });
});

compressor function

given: “aaabbcca”
return: “a3b2c2a1”

def compressor(string)
	# initialize
	compressed = ''
	count = 0
	i = 0

	size = string.size

	string.each_char do |s|
		
		i += 1

		if compressed[-1].nil?
			count = 1
			compressed = s
			next
		end

		# when c[-1] = a, s = a
		if compressed[-1] == s 
			count += 1
			# last character
			if i == size
				compressed << count.to_s
			end
			next
		end

		# when c[-1] = a, s = b
		if compressed[-1] !=s
			compressed << count.to_s << s
			count = 1
			next
		end

	end
	compressed
end

angularjs rails simple_form name

when using rails simple_form and angularjs for form validation

= simple_form_for :address, url: account_path(account.id), 
                            method: :patch, wrapper: :custom,
                            defaults: { input_html: { required: true } },
                            html: { name: 'addressForm',
                                    novalidate: true,
                                    'ng-submit' => "addressCtrl.submitForm(addressForm, $event)" } do |f|

  %fieldset.form-fieldset
    = f.input :address_1, input_html: { name: 'contact[street_address_1]', 
                                        'ng-model' => 'addressCtrl.address.street_address_1', 
                                        'ng-init' => "addressCtrl.address.street_address_1='#{account.street_address_1}'" }, 
                          wrapper_html: { 'ng-class' => "{'form-field--error': addressForm['contact[street_address_1]'].$invalid && addressForm['contact[street_address_1]'].$touched}" },
                          error_wrapper_html: { 'translate' => 'address.error.required.street_address_1', 
                                                'ng-show' => "addressForm['contact[street_address_1]'].$error.required && addressForm['contact[street_address_1]'].$touched" }

...

given input name

  name: 'contact[street_address_1]'

angularjs calls it by:

  addressForm['contact[street_address_1]']

Find all prime numbers between x & y

I was watching this today… Learn Ruby Programming – Day 17 – Find Prime Numbers

well almost half way through before I decided to run it myself
note: it’s not an exact copy but the core is the same

def find_prime(x,y)
  prime = []
  while (x <= y)
    prime_flag = true
    i = 2
    while (i <= x/2)
      if x%i == 0 
        prime_flag = false
        break
      end
      i +=1
    end
    if prime_flag
      prime << x
    end
    x+=1
  end
  prime
end

and someone suggested to use #find

def prime_between(x,y)
  prime = []
  while x <= y
    result = (2..x).find{|i| x%i == 0}
    prime << x if result == x
    x +=1
  end
  prime
end

Yep #prime_between is definitely much neater than #find_prime, but how about the time?

t = Time.now
10.times{find_prime(7,100)}
puts "#find_prime: #{Time.now - t}"

t = Time.now
10.times{prime_between(7,100)}
puts "#prime_between: #{Time.now - t}"

#and in case you wanna see benchmark

require "benchmark"
time = Benchmark.measure do
  find_prime(7,100)
end
puts time

time1 = Benchmark.measure do
  prime_between(7,100)
end
puts time1

————- result

time 1: 0.000462
time 2: 0.001452
——————-
0.000000 0.000000 0.000000 ( 0.000052)
0.000000 0.000000 0.000000 ( 0.000167)

So even though #prime_between is neater, it’s slower… noted that #find_prime goes up to x/2

#...
while (i <= x/2)

whereas #prime_between goes up to x

#...
result = (2..x).find{|i| x%i == 0}

so in worst case scenario, #prime_between is obviously going to run longer…

so I updated #prime_between

def prime_between(x,y)
  prime = []
  while x <= y
    result = (2..x/2).find{|i| x%i == 0}
    prime << x if result.nil?
    x +=1
  end
  prime
end

updated benchmark:
#find_prime: 0.000463
#prime_between: 0.001015
——————-
0.000000 0.000000 0.000000 ( 0.000052)
0.000000 0.000000 0.000000 ( 0.000127)

Hmmm… yep a little faster than the previous #prime_between but still slower than #find_prime.

Anyway, here’s another one inspired by Sieve of Eratosthenes

def sieve_prime(x,y)
  prime = (x..y).to_a
  while (x<=y)
    (2..Math.sqrt(x)).each do |i|
      if x%i == 0
        prime -= [x]
        break
      end
    end
    x +=1
  end
  prime
end