The Evolution of Asset ๐Ÿ“‘ Management in Web and Ruby on Rails

Understanding Middleware in Rails

When a client request comes into a Rails application, it doesn’t always go directly to the MVC (Model-View-Controller) layer. Instead, it might first pass through middleware, which handles tasks such as authentication, logging, and static asset management.

Rails uses middleware like ActionDispatch::Static to efficiently serve static assets before they even reach the main application.

ActionDispatch::Static Documentation

“This middleware serves static files from disk, if available. If no file is found, it hands off to the main app.”

Where Are Static Files Stored?

Rails stores static assets in the public/ directory, and ActionDispatch::Static ensures these are served efficiently without hitting the Rails stack.

Core Components of Ruby on Rails – A reminder

To understand asset management evolution, let’s quickly revisit Rails’ core components:

  • ActiveRecord: Object-relational mapping (ORM) system for database interactions.
  • Action Pack: Handles the controller and view layers.
  • Active Support: A collection of utility classes and standard library extensions.
  • Action Mailer: A framework for designing email services.

The Role of Browsers in Asset Management

Web browsers cache static assets to improve performance. The caching strategy varies based on asset types:

  • Images: Rarely change, so they are aggressively cached.
  • JavaScript and CSS files: Frequently updated, requiring cache-busting mechanisms.

The Era of Sprockets

Historically, Rails used Sprockets as its default asset pipeline. Sprockets provided:

  • Conversion of CoffeeScript to JavaScript and SCSS to CSS.
  • Minification and bundling of assets into fewer files.
  • Digest-based caching to ensure updated assets were fetched when changed.

The Rise of JavaScript & The Shift Towards Webpack

The release of ES6 (2015-2016) was a turning point for JavaScript, fueling the rise of Single Page Applications (SPAs). This marked a shift from traditional asset management:

  • Sprockets was effective but became complex and difficult to configure for modern JS frameworks.
  • Projects started including package.json at the root, indicating JavaScript dependency management.
  • Webpack emerged as the go-to tool for handling JavaScript, offering features like tree-shaking, hot module replacement, and modern JavaScript syntax support.

The Landscape in 2024: A More Simplified Approach

Recent advancements in web technology have drastically simplified asset management:

  1. ES6 Native Support in All Major Browsers
    • No need for transpilation of modern JavaScript.
  2. CSS Advancements
    • Features like variables and nesting eliminate the need for preprocessors like SASS.
  3. HTTP/2 and Multiplexing
    • Enables parallel loading of multiple assets over a single connection, reducing dependency on bundling strategies.

Enter Propshaft: The Modern Asset Pipeline

Propshaft is the new asset management solution introduced in Rails, replacing Sprockets for simpler and faster asset handling. Key benefits include:

  • Digest-based file stamping for effective cache busting.
  • Direct and predictable mapping of assets without complex processing.
  • Better integration with HTTP/2 for efficient asset delivery.

Rails 8 Precompile Uses Propshaft

What is Precompile? A Reminder

Precompilation hashes all file names and places them in the public/ folder, making them accessible to the public.

Propshaft improves upon this by creating a manifest file that maps the original filename as a key and the hashed filename as a value. This significantly enhances the developer experience in Rails.

Propshaft ultimately moves asset management in Rails to the next level, making it more efficient and streamlined.

The Future of Asset Management in Rails

With advancements like native ES6 support and CSS improvements, Rails continues evolving to embrace simpler, more efficient asset management strategies. Propshaft, combined with modern browser capabilities, makes asset handling seamless and more performance-oriented.

As the web progresses, we can expect further simplifications in asset pipelines, making Rails applications faster and easier to maintain.

Stay tuned for more innovations in the Rails ecosystem!

Happy Rails Coding! ๐Ÿš€

JavaScript / ES6+ Learning – Lesson 6 – this, call, apply, bind, Arrow Functions and Closures

This is one of the most important JavaScript lessons for us as a Ruby/Rails developer.

After this lesson, we should be able to read code like:

const user = {
name: "John",
greet() {
console.log(this.name);
}
};

and understand exactly what this means.

We’ll also understand why:

const greet = user.greet;

can behave differently from:

user.greet();

Then we’ll connect that to:

useEffect(() => {
...
});

and eventually to closures, which are fundamental to React and Node.js.


1. What is this?

The easiest starting point is:

this is a special value available inside a function.

But unlike Ruby’s self, its value is usually determined by how a regular function is called.

Consider:

const user = {
name: "John",
greet() {
console.log(this.name);
}
};
user.greet();

Output:

John

Why?

Because we called:

user.greet();

The object before the . is the receiver.

So:

user.greet()
โ†“
this = user

Therefore:

this.name

is effectively:

user.name

2. Think about the call site

This is the most useful mental model:

user.greet();

Look at the left side of the dot:

user . greet()
^^^^
receiver

For a normal method call, that receiver becomes this.

Example:

const person = {
name: "Alice",
sayName() {
console.log(this.name);
}
};
person.sayName();

Here:

this โ†’ person

Output:

Alice

3. But this is not permanently attached to the function

This is where JavaScript differs from the simple mental model we may have from Ruby.

Take:

const user = {
name: "John",
greet() {
console.log(this.name);
}
};

Now:

user.greet();

gives:

John

But:

const greet = user.greet;
greet();

is a different call.

We did:

const greet = user.greet;

So greet now refers to the function.

But we’re no longer calling it as:

user.greet()

We’re calling:

greet()

There is no receiver.

So the this behavior is different.


4. The three things we should distinguish

When we see a function, distinguish:

user.greet
user.greet()
const fn = user.greet;
fn();

They mean different things.

user.greet

Get the function.

user.greet()

Call the function with user as the method receiver.

fn()

Call the function independently.

This distinction is extremely important in JavaScript.


5. this in a regular function

Consider:

function showThis() {
console.log(this);
}

What this is depends on how we invoke the function.

That’s the core idea:

regular function
โ†“
how was it called?
โ†“
determines `this`

There are several invocation patterns, and we’ll learn the important ones.


6. Method call

The easiest:

const user = {
name: "John",
greet() {
console.log(this.name);
}
};
user.greet();

Here:

this = user

7. Explicitly set this

JavaScript gives us:

call()
apply()
bind()

These let us control what this refers to.

This is a very common int. topic.


8. call

Suppose:

function greet() {
console.log(this.name);
}

We have an object:

const user = {
name: "John"
};

We can explicitly say:

greet.call(user);

Now:

this = user

Therefore:

John

So:

greet.call(user);

means approximately:

Call greet now, and make this equal to user.


9. call with arguments

Suppose:

function greet(message) {
console.log(`${message}, ${this.name}`);
}

Then:

const user = {
name: "John"
};

Call:

greet.call(user, "Hello");

Output:

Hello, John

The structure is:

function.call(thisArg, arg1, arg2, ...)

For example:

greet.call(user, "Hello");

means:

this = user
message = "Hello"

10. apply

apply is similar to call.

greet.apply(user, ["Hello"]);

Output:

Hello, John

The difference is mainly how arguments are supplied.

call

greet.call(user, "Hello", "How are you?");

apply

greet.apply(user, ["Hello", "How are you?"]);

So:

call โ†’ arguments individually
apply โ†’ arguments as an array

Historically apply was particularly useful when we already had an array of arguments. With modern JavaScript, spread syntax has reduced some of those use cases.


11. bind

bind is different.

const boundGreet = greet.bind(user);

This does not immediately call greet.

Instead it creates a new function.

Think:

greet.bind(user)
โ†“
new function
โ†“
this permanently set to user

Then:

boundGreet("Hello");

Output:

Hello, John

So:

call โ†’ call now
apply โ†’ call now
bind โ†’ create another function

That’s the key distinction.


12. call vs apply vs bind

Memorize this table:

MethodExecutes immediately?Arguments
callYesIndividual
applyYesArray
bindNoReturns new function

Example:

fn.call(obj, 1, 2);
fn.apply(obj, [1, 2]);
const newFn = fn.bind(obj);
newFn(1, 2);

13. A practical example

Suppose:

const user1 = {
name: "John"
};
const user2 = {
name: "Jane"
};
function greet(message) {
console.log(`${message}, ${this.name}`);
}

Now:

greet.call(user1, "Hello");

Output:

Hello, John

And:

greet.call(user2, "Hello");

Output:

Hello, Jane

Same function.

Different this.

This demonstrates why this in regular functions is associated with the invocation.


14. Arrow functions change the rules

Now comes a critical concept.

Arrow functions do not get their own this.

They use this from the surrounding lexical scope.

Example:

const user = {
name: "John",
greet: () => {
console.log(this.name);
}
};

We might think:

user.greet()
โ†“
this = user

But that’s not how an arrow function works.

An arrow function does not create its own this.

It captures this from the surrounding scope.


15. Why this matters

Compare:

Regular function

const user = {
name: "John",
greet() {
console.log(this.name);
}
};
user.greet();

Here:

this = user

Arrow function

const user = {
name: "John",
greet: () => {
console.log(this.name);
}
};

Here this does not become user.

This is why object methods generally should not be written as arrow functions when they need dynamic this.


16. The classic callback problem

Now we get to a very common JavaScript issue.

Suppose:

const user = {
name: "John",
greet() {
setTimeout(function() {
console.log(this.name);
}, 1000);
}
};
user.greet();

We might expect:

John

But the inner regular function has its own this behavior.

The setTimeout callback isn’t being called as:

user.someMethod()

So it doesn’t automatically inherit the outer method’s this.


17. Arrow functions solve this nicely

Now:

const user = {
name: "John",
greet() {
setTimeout(() => {
console.log(this.name);
}, 1000);
}
};
user.greet();

Now the arrow function:

() => {
console.log(this.name);
}

doesn’t create a new this.

It captures the this from greet().

So:

user.greet()
โ†“
this = user
โ†“
arrow callback
โ†“
uses surrounding this
โ†“
this = user

Output:

John

This is one of the biggest reasons arrow functions are so useful.


18. Arrow functions and React

We’ll often see:

<button onClick={() => handleClick()}>
Click
</button>

Here the arrow function is primarily being used as a callback.

React will call the arrow function later when the event occurs.

Similarly:

useEffect(() => {
loadUsers();
}, []);

The callback passed to useEffect is an arrow function.

Again, remember:

() => {
...
}

is just a function.

React receives that function and decides when to invoke it.


19. Now: closures

This is perhaps even more important than this.

A closure happens when a function remembers variables from its surrounding lexical scope.

Consider:

function createCounter() {
let count = 0;
return function() {
count++;
return count;
};
}

Call:

const counter = createCounter();

Now:

counter(); // 1
counter(); // 2
counter(); // 3

The amazing part is:

createCounter() has already finished executing.

Yet the returned function can still access:

count

Why?

Because the function closed over the variable.


20. Visualize the closure

When we execute:

const counter = createCounter();

think:

createCounter()
|
| creates
v
count = 0
|
| returns function
v
counter โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ†’ function
|
โ””โ”€โ”€ remembers count

Then:

counter();

the function can still access the remembered count.

After:

counter();

the value becomes:

count = 1

Next:

counter();

the function still has access to the same variable:

count = 2

21. Closure does NOT mean copying the value

This is an important detail.

Some beginners imagine:

count = 0

gets copied into the function.

That’s not the best mental model.

Instead, the function retains access to the lexical environment containing count.

Therefore:

counter();
counter();
counter();

all operate on the same captured variable.


22. Why closures are useful

Closures allow us to keep private state.

Example:

function createBankAccount(initialBalance) {
let balance = initialBalance;
return {
deposit(amount) {
balance += amount;
},
getBalance() {
return balance;
}
};
}

Now:

const account = createBankAccount(1000);
account.deposit(500);
console.log(account.getBalance());

Result:

1500

But:

account.balance

doesn’t exist as a public property.

The state is captured by the closure.

This is conceptually similar to encapsulation.


23. Closures and callbacks

Callbacks frequently create closures.

Example:

Output:

Hello John

The returned callback remembers:

name = "John"

even after greetUser() finished.

So:

outer function
โ†“
creates variable
โ†“
creates callback
โ†“
callback remembers variable

That’s a closure.


24. Closures in loops

Here’s a classic int. example:

for (var i = 0; i < 3; i++) {
setTimeout(() => {
console.log(i);
}, 1000);
}

Many people expect:

0
1
2

but with var, all callbacks refer to the same function-scoped i, which has reached 3 when the callbacks run.

So we get:

3
3
3

This is a classic closure + asynchronous execution int. question.

Now change var to let:

for (let i = 0; i < 3; i++) {
setTimeout(() => {
console.log(i);
}, 1000);
}

Now the callbacks see the per-iteration i values:

0
1
2

This connects directly to our Lesson 2 discussion of let and block/iteration scoping.


25. Closure + React

Closures are everywhere in React.

For example:

function User({ user }) {
const handleClick = () => {
console.log(user.name);
};
return (
<button onClick={handleClick}>
Click
</button>
);
}

The function:

handleClick

uses:

user

from its surrounding scope.

So it forms a closure over user.

This is normal JavaScript behavior, not React-specific magic.


26. Closure + useEffect

Consider:

function UserComponent({ userId }) {
useEffect(() => {
console.log(userId);
}, [userId]);
return null;
}

The callback passed to useEffect accesses:

userId

from the surrounding function scope.

That’s a closure.

This is why closures and React hooks are closely related.

Later, when we study the dependency array deeply, this will become very important.


27. Closure + Node.js

Closures are equally useful in Node.

For example:

function createLogger(prefix) {
return message => {
console.log(`[${prefix}] ${message}`);
};
}
const errorLogger = createLogger("ERROR");
const infoLogger = createLogger("INFO");
errorLogger("Database failed");
infoLogger("Server started");

Output:

[ERROR] Database failed
[INFO] Server started

Each returned function remembers its own prefix.

So closures are not merely “React stuff.”

They’re a fundamental JavaScript feature.


28. this vs closure

These are different concepts.

this

Answers:

What object/context does this regular function call use as this?

Closure

Answers:

What variables from surrounding lexical scopes can this function still access?

For example:

const user = {
name: "John",
greet() {
const message = "Hello";
setTimeout(() => {
console.log(this.name);
console.log(message);
}, 1000);
}
};

The arrow callback has access to:

this
message

but for different reasons:

this โ†’ inherited from surrounding context
message โ†’ closure over surrounding lexical variable

This distinction is worth remembering.


29. Ruby comparison

Ruby has:

self

JavaScript has:

this

But don’t assume:

Ruby self == JavaScript this

They have important differences.

Ruby self is much more naturally tied to the current execution context/object.

JavaScript this for a regular function is heavily dependent on how the function is called.

And JavaScript arrow functions don’t create their own this.

That distinction is especially important when moving from Rails backend code to React/Node.


30. A very useful mental model

For regular functions, ask:

How was this function called?

For arrow functions, ask:

What is the surrounding `this`?

For closures, ask:

Which variables from outer scopes does this function use?

These three questions will solve a huge number of JavaScript puzzles.


๐ŸŽฏ Interview questions

Try these without running them.

Q1

What is this here?

const user = {
name: "John",
greet() {
console.log(this.name);
}
};
user.greet();

Q2

What’s the problem here?

const greet = user.greet;
greet();

Q3

What’s the difference between:

greet.call(user);

and:

const fn = greet.bind(user);

Q4

What is the difference between call and apply?

Q5

What is a closure?

Q6

Why does this work?

function createCounter() {
let count = 0;
return () => ++count;
}
const counter = createCounter();
counter(); // ?
counter(); // ?

Q7

Why does var produce the classic loop problem?

for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}

๐Ÿงช Live coding practice

Problem 1 – this

Predict the output:

const user = {
name: "John",
greet() {
return `Hello ${this.name}`;
}
};
console.log(user.greet());

Problem 2 – call

Write:

function introduce(role) {
console.log(`${this.name} is a ${role}`);
}

Then use call so that:

const user = {
name: "Abhilash"
};

produces:

Abhilash is a Senior Software Engineer

Problem 3 – bind

Given:

const user = {
name: "John"
};
function greet(message) {
console.log(`${message}, ${this.name}`);
}

Create:

const boundGreet = ...

so that:

boundGreet("Hello");

produces:

Hello, John

Problem 4 – Closure

Implement:

const counter = createCounter();
counter(); // 1
counter(); // 2
counter(); // 3

The count variable should not be directly accessible from outside.


Problem 5 – Closure with argument

Create:

const double = multiplier(2);
double(5); // 10
double(10); // 20

The function multiplier(2) should return another function.

This is an excellent exercise for understanding closures.


Problem 6 – Callback + closure

Implement:

function createGreeter(name) {
return function() {
console.log(`Hello ${name}`);
};
}

Then:

const greetJohn = createGreeter("John");
const greetJane = createGreeter("Jane");
greetJohn();
greetJane();

Expected:

Hello John
Hello Jane

Be able to explain why each returned function remembers a different name.


Final mental model

We now have four layers:

FUNCTION
โ†“
Function can be passed around as a value
CALLBACK
โ†“
A function passed to another function
THIS
โ†“
For regular functions, determined by how the function is called
CLOSURE
โ†“
A function retains access to variables from its lexical scope

And the most important distinctions:

user.greet
โ†“
function reference
user.greet()
โ†“
function call
โ†“
this = user
greet.call(user)
โ†“
call now with explicit this
greet.apply(user, args)
โ†“
call now with explicit this + array arguments
greet.bind(user)
โ†“
return a new function with bound this

For arrow functions:

arrow function
โ†“
doesn't create its own `this`
โ†“
captures surrounding `this`

For closures:

outer scope
โ†“
function created inside it
โ†“
inner function remembers outer variables

The connection to React

We can now look at:

function User({ user }) {
const handleClick = () => {
console.log(user.name);
};
return (
<button onClick={handleClick}>
Click
</button>
);
}

and identify:

{ user } โ†’ object destructuring
handleClick โ†’ function
() => โ†’ arrow function
onClick={handleClick}โ†’ function reference
user.name โ†’ closure over user

That’s the exact JavaScript foundation we want before going further into React.

Next lesson: JavaScript asynchronous programming – synchronous execution, call stack, event loop, Web/Node APIs, callback queue, microtask queue, Promises, and why async/await works. This will be especially important for us because it connects JavaScript directly to Node.js.

Lesson 7: https://railsdrop.com/2024/12/10/javascript-es6-learning-lesson-7-asynchronous-javascript-event-loop-promises-and-async-await/

JavaScript / ES6+ Learning – Lesson 2

This lesson is important because JavaScript variable scope behaves differently enough from Ruby to cause bugs and int. confusion.

The four things to understand:

let
const
scope
hoisting

let, const, Scope and Hoisting

1. let vs const

Start with the simplest rule:

const name = "Abhilash";
let age = 40;

Use const when the variable should not be reassigned:

const name = "Abhilash";
// name = "John"; โŒ

Use let when reassignment is required:

let age = 40;
age = 41; // โœ…

Important: const does NOT make the value immutable

This is a very important JavaScript int. question.

const users = [];
users.push("John"); // โœ…

The array can be modified.

What cannot happen is:

users = ["Jane"]; // โŒ

Why?

Because const prevents reassignment of the variable, not mutation of the object.

Think:

const users
|
v
[ "John" ]

You cannot make users point somewhere else:

users โ”€โ”€โ”€โ”€โ”€Xโ”€โ”€โ”€โ”€> [ "Jane" ]

But you can modify the existing array:

users
|
v
["John", "Jane"]

The same applies to objects:

const user = {
name: "John"
};
user.name = "Jane"; // โœ…

But:

user = {}; // โŒ

Int. answer

const prevents reassignment, but it does not make objects or arrays immutable.


2. What is scope?

Scope simply means:

Where can this variable be accessed?

Example:

const name = "Abhilash";
function greet() {
console.log(name);
}
greet();

name is available inside greet() because functions can access variables from their outer scope.

But:

function greet() {
const message = "Hello";
}
console.log(message); // โŒ

message exists only inside the function.

Visualize:

Global Scope
โ”‚
โ”œโ”€โ”€ name
โ”‚
โ””โ”€โ”€ greet()
โ”‚
โ””โ”€โ”€ message

greet() can see name.

The outside cannot see message.


3. Block scope

This is where let and const become important.

A block is anything inside { }, such as:

if (...)
{
}

or:

for (...)
{
}

Example:

if (true) {
const message = "Hello";
console.log(message); // โœ…
}
console.log(message); // โŒ

message is block scoped.

Same with let:

if (true) {
let age = 40;
}
console.log(age); // โŒ

4. Why var is different

Historically JavaScript used:

var name = "John";

var does not have block scope.

Example:

if (true) {
var name = "John";
}
console.log(name); // John

That surprises many developers.

Compare:

if (true) {
let name = "John";
}
console.log(name); // โŒ

This is one major reason modern JavaScript prefers let and const.


5. Ruby comparison

You may initially think:

if true
name = "John"
end
puts name

Ruby’s local-variable behavior is different from JavaScript’s block scoping rules.

For JavaScript, remember:

const + let
โ†“
block scoped

and:

var
โ†“
function scoped

You don’t need to use var in modern code, but you must understand it when reading legacy JavaScript.


6. Nested scope

Scopes can be nested.

const country = "India";
function outer() {
const state = "Kerala";
function inner() {
const city = "Kochi";
console.log(country);
console.log(state);
console.log(city);
}
inner();
}

inner() can access:

country
state
city

because it can look outward through its scope chain.

But outer() cannot access city.

Visualize:

Global
โ”‚
โ”œโ”€โ”€ country
โ”‚
โ””โ”€โ”€ outer
โ”‚
โ”œโ”€โ”€ state
โ”‚
โ””โ”€โ”€ inner
โ”‚
โ””โ”€โ”€ city

JavaScript searches for variables from the current scope outward.

This is called the scope chain.


7. A very important example

Look carefully:

const name = "Abhilash";
function greet() {
const message = `Hello ${name}`;
console.log(message);
}
greet();

Inside greet():

name

is not declared locally.

JavaScript looks outward:

greet scope
โ†“
global scope
โ†“
find name

This behavior becomes extremely important when we learn closures.


8. What is hoisting?

Now we get to a classic int. topic.

JavaScript processes declarations before executing the code in the current scope.

This behavior is commonly called hoisting.

But don’t interpret it as JavaScript literally moving your code to the top. That’s a useful mental model, but the actual mechanics are more nuanced.

Start with a function declaration:

greet();
function greet() {
console.log("Hello");
}

This works.

Why?

Function declarations are available before their textual position.


9. var and hoisting

Consider:

console.log(name);
var name = "John";

You might expect an error.

Instead:

undefined

A useful mental model is:

var name;
console.log(name);
name = "John";

The declaration is available, but the assignment happens later.


10. let and const are different

Now:

console.log(name);
let name = "John";

This produces:

ReferenceError

And:

console.log(name);
const name = "John";

also produces:

ReferenceError

This is often explained using the Temporal Dead Zone (TDZ).


11. Temporal Dead Zone

For let and const, the variable exists in the scope before its declaration is executed, but you cannot access it before that point.

Example:

console.log(age); // โŒ
let age = 40;

The area between entering the scope and reaching the declaration is called the:

Temporal Dead Zone

You don’t need to memorize the implementation details yet. The int.-level mental model is:

var
โ†“
hoisted + initialized as undefined
let / const
โ†“
hoisted but inaccessible until declaration is reached

12. Function declarations vs function expressions

This becomes important when we get to callbacks.

This works:

greet();
function greet() {
console.log("Hello");
}

But this does not:

greet();
const greet = function() {
console.log("Hello");
};

And similarly:

greet();
const greet = () => {
console.log("Hello");
};

The second forms involve a const variable, so the TDZ applies.


13. A common int. trap

What does this print?

var x = 10;
if (true) {
var x = 20;
}
console.log(x);

Answer:

20

Because var is function scoped, not block scoped.

Now:

let x = 10;
if (true) {
let x = 20;
}
console.log(x);

Answer:

10

The inner x belongs to the block.

Visualize:

let x = 10
โ”‚
โ”œโ”€โ”€ if block
โ”‚ โ””โ”€โ”€ let x = 20
โ”‚
โ””โ”€โ”€ outside -> x is still 10

14. Shadowing

JavaScript allows an inner scope to define a variable with the same name.

const name = "Abhilash";
function test() {
const name = "John";
console.log(name);
}
test();
console.log(name);

Output:

John
Abhilash

The inner variable shadows the outer one.

This is perfectly valid, although unnecessary shadowing can make code harder to read.


15. Why this matters in React

Consider:

function UserList() {
const [users, setUsers] = useState([]);
if (users.length === 0) {
const message = "No users";
return <p>{message}</p>;
}
return (
<ul>
...
</ul>
);
}

message exists only inside that if block.

React code frequently contains nested blocks, callbacks and functions, so understanding scope prevents many bugs.


16. Scope + callbacks = important later

Consider:

function createCounter() {
let count = 0;
return function() {
count++;
console.log(count);
};
}

Don’t worry about understanding every detail yet.

The interesting question is:

How can the returned function still access count after createCounter() has finished?

That question leads directly to:

closures.

And closures are one of the most important JavaScript concepts for React and Node.js.


Qn) Is it necessary to put semi colon in Javascript?

No, putting a semicolon (or colon) at the end of a line is not mandatory in ES6 (ECMAScript 2015) or modern JavaScript.

JavaScript uses a feature called Automatic Semicolon Insertion (ASI). This means the JavaScript engine automatically inserts semicolons where it thinks they are needed to run your code correctly.

โš ๏ธ The Rare Exception

While you can safely omit semicolons 99% of the time, there are rare cases where leaving them out can break your code. This usually happens if a line starts with a bracket [ or parenthesis (.

Example of a bug without semicolons:

const user = "Alice"
['a', 'b'].forEach(letter => console.log(letter))

The JavaScript engine reads this as a single continuous line: const user = "Alice"['a', 'b'].forEach(...), which throws an error.

Best Practice

Because of ASI, choosing to use semicolons is largely a matter of personal or team preference:

  • With semicolons: Safer for beginners, prevents accidental ASI bugs, and follows traditional coding styles.
  • Without semicolons: Keeps code looking clean, modern, and reduces visual clutter.

Most development teams use a tool like Prettier or ESLint to automatically format the code and handle semicolons for them.

Int. questions

Try answering these without looking back.

Q1

What is the difference between:

const user = {};

and:

let user = {};

Q2

Why does this work?

const users = [];
users.push("John");

even though users is declared with const?

Q3

What is the output?

let x = 10;
if (true) {
let x = 20;
}
console.log(x);

Q4

What is the output?

var x = 10;
if (true) {
var x = 20;
}
console.log(x);

Q5

What happens here?

console.log(name);
let name = "John";

Q6

Why does this work?

greet();
function greet() {
console.log("Hello");
}

but this doesn’t?

greet();
const greet = () => {
console.log("Hello");
};

Mini exercise

Predict the output before running:

const name = "Abhilash";
function outer() {
const name = "John";
if (true) {
const name = "Jane";
console.log(name);
}
console.log(name);
}
outer();
console.log(name);

Expected mental trace:

global name
โ†“
outer name
โ†“
if-block name

Check the detailed Answer here: https://railsdrop.com/is-javascript-synchronous-or-asynchronous/

The most important thing from this lesson is to build the habit of asking:

Which scope does this variable belong to?

That question will help enormously when we reach closures, callbacks, useEffect, event handlers and async JavaScript.

Next lesson: Functions in depth – function declarations, expressions, arrow functions, parameters, return values, callbacks and higher-order functions.

Lesson 3: https://railsdrop.com/2024/12/04/javascript-es6-bootcamp-lesson-3-javascript-functions-arrow-functions-and-callbacks/

JavaScript / ES6+ Learning – Lesson 1

The goal of this lesson is simple: understand what JavaScript is actually doing when you write basic code.

Coming from Ruby, you’ll notice that many concepts are similar, but JavaScript has some syntax and behavior that becomes important later in React and Node.js.

Variables, Values, Objects, Arrays, Functions and Return Values

1. Variables: let and const

In modern JavaScript, primarily use:

const name = "Abhilash";
let age = 40;

Think of them roughly like Ruby local variables:

name = "Abhilash"
age = 40

The difference is important:

const

The variable cannot be reassigned:

const name = "Abhilash";
name = "John"; // TypeError

let

The variable can be reassigned:

let age = 40;
age = 41;

What about var?

You’ll see:

var name = "Abhilash";

in older JavaScript code.

For modern JavaScript:

Use const by default.
Use let when reassignment is required.
Avoid var unless you're dealing with legacy code.

2. Everything starts with values

JavaScript variables hold values.

const name = "Abhilash";
const age = 40;
const active = true;

These values have types.

typeof name; // "string"
typeof age; // "number"
typeof active; // "boolean"

Some common types:

string
number
boolean
undefined
null
object
symbol
bigint

For now, focus on:

string
number
boolean
undefined
null
object

3. Arrays

An array stores multiple values.

const numbers = [10, 20, 30];

You can access them using an index:

numbers[0]; // 10
numbers[1]; // 20
numbers[2]; // 30

Just like Ruby:

numbers = [10, 20, 30]
numbers[0] # 10

JavaScript arrays are zero-indexed too.

Important

This:

const numbers = [10, 20, 30];

means numbers contains one array value.

It does not mean:

numbers = 10
20
30

Think:

numbers
|
v
[10, 20, 30]

4. Objects

JavaScript objects are heavily used in React and Node.

const user = {
id: 1,
name: "Abhilash",
active: true
};

You can access properties:

user.name; // "Abhilash"
user.id; // 1
user.active; // true

Ruby equivalent:

user = {
id: 1,
name: "Abhilash",
active: true
}
user[:name]

JavaScript also supports bracket notation:

user["name"];

This becomes useful when the property name is dynamic.


5. Arrays can contain objects

This is extremely common in React.

const users = [
{ id: 1, name: "John" },
{ id: 2, name: "Jane" },
{ id: 3, name: "Mike" }
];

Visualize it:

users
|
v
[
{ id: 1, name: "John" },
{ id: 2, name: "Jane" },
{ id: 3, name: "Mike" }
]

Then:

users[0].name;

returns:

"John"

This structure is everywhere in frontend applications.


6. Functions

A JavaScript function can be written like:

function add(a, b) {
return a + b;
}

Call it:

const result = add(10, 20);
console.log(result); // 30

Ruby equivalent:

def add(a, b)
a + b
end
result = add(10, 20)

So far, very familiar.


7. The important idea: functions are values

This is one of the biggest concepts you need for React and Node.

In JavaScript:

function add(a, b) {
return a + b;
}

The function itself is a value.

You can store it:

const operation = add;

Now:

operation(10, 20);

returns:

30

Visualize:

add
|
v
function
operation
|
+------> same function

This is why JavaScript can pass functions around so easily.


8. Functions can return anything

A function can return a number:

function getAge() {
return 40;
}

A string:

function getName() {
return "Abhilash";
}

An object:

function getUser() {
return {
id: 1,
name: "John"
};
}

An array:

function getUsers() {
return [
{ id: 1, name: "John" },
{ id: 2, name: "Jane" }
];
}

And importantly, a function can return another function:

function createGreeter() {
return function() {
console.log("Hello");
};
}

We’ll use this concept later when learning closures.


9. This explains useState

Now let’s return to the example that confused you:

const [users, setUsers] = useState([]);

Ignore React for a moment.

Imagine an ordinary function:

function useState(initialValue) {
return [
initialValue,
function setValue(value) {
console.log(value);
}
];
}

Now call:

const result = useState([]);

What does result contain?

Conceptually:

[
[],
setValueFunction
]

So:

const users = result[0];
const setUsers = result[1];

Now combine the two operations with destructuring:

const [users, setUsers] = result;

Therefore:

const [users, setUsers] = useState([]);

means:

1. Pass [] into useState
2. useState returns [value, function]
3. destructure that returned array
4. users gets element 0
5. setUsers gets element 1

This is a critical mental model.


10. Array destructuring

Let’s isolate the JavaScript feature.

const numbers = [10, 20];
const [a, b] = numbers;

Equivalent to:

const a = numbers[0];
const b = numbers[1];

You can even ignore values:

const [first, , third] = [10, 20, 30];
console.log(first); // 10
console.log(third); // 30

11. Object destructuring

There is another extremely common form:

const user = {
name: "John",
age: 30
};
const { name, age } = user;

Equivalent to:

const name = user.name;
const age = user.age;

So remember:

Array -> []
Object -> {}

Therefore:

const [a, b] = array;
const { name, age } = object;

This distinction is extremely important in React.


12. Arrow functions

Modern JavaScript frequently uses:

const add = (a, b) => {
return a + b;
};

Short form:

const add = (a, b) => a + b;

Equivalent roughly to:

function add(a, b) {
return a + b;
}

React code uses arrow functions constantly:

users.map(user => user.name);

Don’t worry about map yet. Just notice:

user => user.name

is a function.


13. Why callbacks matter

Consider:

function execute(callback) {
callback();
}

Then:

execute(() => {
console.log("Hello");
});

What’s happening?

execute()
|
| receives a function
v
callback
|
v
callback()
|
v
console.log("Hello")

The function is being passed as a value.

This is the foundation of callbacks.

We’ll go much deeper into this in a later lesson.


Ruby developer mental model

For now, keep these mappings in your head:

JavaScriptRuby
const x = 10x = 10
[]Array
{}Hash-like object
functiondef / Proc/Lambda concepts
returnreturn
obj.nameobj[:name] for Hash
array[0]array[0]
function as valueProc/lambda/block-like concept
arrow functionlambda-ish syntax, but not identical

The last row is deliberately approximate. JavaScript functions, arrow functions, Ruby blocks, Procs and lambdas are not interchangeable concepts. We’ll cover the differences properly.


๐ŸŽฏ Lesson 1 int. questions

Try answering these without looking back.

Question 1

What does this return?

const numbers = [10, 20, 30];
numbers[1];

Question 2

What is the difference between:

const user = {
name: "John"
};

and:

const users = [
{ name: "John" }
];

Question 3

What does this do?

const [a, b] = [10, 20];

Question 4

What does this return?

function getUser() {
return {
id: 1,
name: "John"
};
}

Question 5

What is stored in operation?

function add(a, b) {
return a + b;
}
const operation = add;

Small exercise

Without running it, predict the output:

function getUser() {
return [
{ id: 1, name: "John" },
{ id: 2, name: "Jane" }
];
}
const users = getUser();
const [firstUser, secondUser] = users;
console.log(firstUser.name);
console.log(secondUser.name);

Then try this variation:

const [firstUser] = getUser();
console.log(firstUser.name);

The key skill I’m looking for is not memorizing syntax. It’s being able to mentally trace:

function call
โ†“
return value
โ†“
array/object
โ†“
destructuring
โ†“
variables

Once that becomes natural, a lot of React code will stop looking mysterious.

Next lesson: let, const, scope, hoisting, and why JavaScript behaves differently from Ruby around variable scope.

Happy Learning! to be continued..